├── VERSION ├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── moving_window_test.py │ ├── decorator_test.py │ ├── fixed_window_test.py │ └── fastapi_utils_test.py ├── acceptance │ ├── __init__.py │ ├── steps │ │ ├── __init__.py │ │ └── fastapi.py │ ├── acceptance_test.py │ └── features │ │ └── fastapi_limiter.feature ├── utils_for_test.py └── conftest.py ├── throttled ├── py.typed ├── __init__.py ├── strategies.py ├── exceptions.py ├── fastapi │ ├── __init__.py │ ├── limiters.py │ ├── utils.py │ └── base.py ├── storage │ ├── __init__.py │ ├── _duration.py │ ├── _abstract.py │ ├── memory.py │ └── redis.py ├── models.py └── limiter.py ├── sonar-project.properties ├── .gitignore ├── Makefile ├── design └── hit.puml ├── .github └── workflows │ ├── contribute.yml │ └── publish.yml ├── LICENSE ├── .pre-commit-config.yaml ├── pyproject.toml ├── CHANGELOG.md ├── README.md ├── .pylint.cfg └── poetry.lock /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.1 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /throttled/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/acceptance/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/acceptance/steps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /throttled/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.1" 2 | -------------------------------------------------------------------------------- /throttled/strategies.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | 4 | class Strategies(Enum): 5 | FIXED_WINDOW = auto() 6 | MOVING_WINDOW = auto() 7 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=santunioni_ThrottledAPI 2 | sonar.organization=santunioni 3 | sonar.sources=. 4 | sonar.python.coverage.reportPaths=coverage.xml 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/coverage/ 2 | **/.pytest_cache 3 | **/.mypy_cache/ 4 | **/__pycache__/ 5 | **/.coverage 6 | **/.idea/ 7 | **/.tox/ 8 | **/dist/ 9 | **/.vscode/ 10 | coverage.xml 11 | -------------------------------------------------------------------------------- /throttled/exceptions.py: -------------------------------------------------------------------------------- 1 | class RateLimitExceeded(Exception): 2 | def __init__(self, key: str, retry_after: float): 3 | self.key = key 4 | self.retry_after = retry_after 5 | super().__init__() 6 | -------------------------------------------------------------------------------- /throttled/fastapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import FastAPILimiter, MiddlewareLimiter 2 | from .limiters import IPLimiter, TotalLimiter 3 | from .utils import split_dependencies_and_middlewares 4 | 5 | __all__ = [ 6 | "TotalLimiter", 7 | "IPLimiter", 8 | "split_dependencies_and_middlewares", 9 | "FastAPILimiter", 10 | "MiddlewareLimiter", 11 | ] 12 | -------------------------------------------------------------------------------- /throttled/fastapi/limiters.py: -------------------------------------------------------------------------------- 1 | from starlette.requests import Request 2 | 3 | from .base import MiddlewareLimiter 4 | 5 | 6 | class IPLimiter(MiddlewareLimiter): 7 | def __call__(self, request: Request): 8 | self.limit(key=f"host={request.client.host}") 9 | 10 | 11 | class TotalLimiter(MiddlewareLimiter): 12 | def __call__(self, request: Request): 13 | self.limit(key="total") 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | @poetry run isort . 3 | @poetry run black . 4 | @poetry run pylint --rcfile=.pylint.cfg throttled 5 | @poetry run pylint --rcfile=.pylint.cfg --disable=redefined-outer-name tests 6 | 7 | mypy: 8 | @poetry run mypy . 9 | 10 | test: 11 | @poetry run pytest tests 12 | 13 | checks: lint mypy test 14 | 15 | git-hooks: 16 | @pre-commit run --all-files --hook-stage merge-commit 17 | 18 | push: 19 | @poetry build 20 | @git push && git push --tags 21 | -------------------------------------------------------------------------------- /throttled/storage/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from throttled.models import Rate 4 | from throttled.storage._abstract import _WindowRepository 5 | from throttled.strategies import Strategies 6 | 7 | 8 | class BaseStorage(ABC): 9 | @abstractmethod 10 | def get_window_repository( 11 | self, strategy: Strategies, limit: Rate 12 | ) -> _WindowRepository: 13 | """Return a _WindowManager for the given strategy and rate limit.""" 14 | -------------------------------------------------------------------------------- /tests/acceptance/acceptance_test.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import scenario 2 | 3 | from ..utils_for_test import get_package_paths_in_module 4 | from . import steps 5 | 6 | pytest_plugins = get_package_paths_in_module(steps) 7 | 8 | 9 | @scenario("features/fastapi_limiter.feature", "The limiter is ignoring a given path") 10 | def test_fastapi_limiter_should_ignore_ignored_paths(): 11 | pass 12 | 13 | 14 | @scenario("features/fastapi_limiter.feature", "There is a global limiter in the API") 15 | def test_fastapi_limiter_should_limiter_excessive_requests(): 16 | pass 17 | -------------------------------------------------------------------------------- /throttled/storage/_duration.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Mapping 2 | 3 | from throttled.strategies import Strategies 4 | 5 | DurationCalcType = Callable[[float, float], float] 6 | 7 | 8 | def fixed_window_duration_calc(hit_time: float, interval: float) -> float: 9 | return interval - hit_time % interval 10 | 11 | 12 | def moving_window_duration_calc(_: float, interval: float) -> float: 13 | return interval 14 | 15 | 16 | DUR_REGISTRY: Mapping[Strategies, DurationCalcType] = { 17 | Strategies.FIXED_WINDOW: fixed_window_duration_calc, 18 | Strategies.MOVING_WINDOW: moving_window_duration_calc, 19 | } 20 | -------------------------------------------------------------------------------- /throttled/models.py: -------------------------------------------------------------------------------- 1 | import time as clock 2 | from dataclasses import dataclass, field 3 | from typing import NamedTuple 4 | 5 | 6 | @dataclass(frozen=True) 7 | class Hit: 8 | key: str = field( 9 | default_factory=str, 10 | metadata={"description": "The base contribution to the hit key."}, 11 | ) 12 | time: float = field( 13 | default_factory=clock.time, 14 | metadata={ 15 | "units": "seconds", 16 | "description": "time the hit occurred since epoch.", 17 | }, 18 | ) 19 | cost: int = 1 20 | 21 | 22 | class Rate(NamedTuple): 23 | hits: int 24 | interval: float 25 | -------------------------------------------------------------------------------- /tests/unit/moving_window_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.utils_for_test import NumbersComparer 4 | from throttled.exceptions import RateLimitExceeded 5 | from throttled.strategies import Strategies 6 | 7 | numbers_almost_equals = NumbersComparer(error=1e-2).almost_equals 8 | 9 | 10 | def test_limiter_should_block_after_reaching_request_limit(limit, limiter_for): 11 | limiter = limiter_for(Strategies.MOVING_WINDOW) 12 | key = "my-custom-key" 13 | 14 | for _ in range(limit.hits): 15 | limiter.limit(key) 16 | 17 | with pytest.raises(RateLimitExceeded) as err: 18 | limiter.limit(key) 19 | 20 | assert numbers_almost_equals(err.value.retry_after, limit.interval) 21 | -------------------------------------------------------------------------------- /tests/acceptance/features/fastapi_limiter.feature: -------------------------------------------------------------------------------- 1 | # Created by santunioni at 2/15/22 2 | Feature: FastAPI rate limiting 3 | 4 | Background: 5 | Given A Rest API built with FastAPI 6 | 7 | 8 | Scenario: There is a global limiter in the API 9 | Given The API is limited globally to 10 requests per 1 seconds 10 | When I hit the API 10 times in a row 11 | Then All responses are successful 12 | But The next hit is blocked by the limiter 13 | And I should only retry after 1 seconds 14 | 15 | 16 | Scenario: The limiter is ignoring a given path 17 | Given The API is limited globally to 5 requests per 1 seconds 18 | But The limiter ignores the path being tested 19 | When I hit the API 30 times in a row 20 | Then All responses are successful 21 | -------------------------------------------------------------------------------- /design/hit.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | class Hit { 3 | + key: str 4 | + cost: int 5 | {field} + time: int 6 | } 7 | 8 | 9 | class Rate { 10 | + hits: int 11 | + interval: int 12 | } 13 | 14 | 15 | abstract class Strategy { 16 | ~ rate: Rate 17 | + get_retry_after(hit: Hit) -> int: 18 | } 19 | Strategy -l-> Hit 20 | Strategy *-r- Rate 21 | class MovingWindow implements Strategy 22 | class FixedWindow implements Strategy 23 | 24 | 25 | class MovingWindow { 26 | + get_retry_after(hit: Hit) -> int: 27 | - get_last_hit() -> Hit 28 | } 29 | 30 | 31 | class Limiter { 32 | - hit_factory: Callable[[], Hit] 33 | - strategy: Strategy 34 | + attempt_hit(hit: Hit) -> bool 35 | } 36 | Limiter -d-> Hit 37 | Limiter -d-> Strategy 38 | 39 | 40 | @enduml 41 | -------------------------------------------------------------------------------- /throttled/fastapi/utils.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Union 2 | 3 | from fastapi import Depends 4 | from starlette.middleware.base import DispatchFunction 5 | 6 | from .base import FastAPILimiter, MiddlewareLimiter 7 | 8 | 9 | def split_dependencies_and_middlewares( 10 | *limiters: Union[FastAPILimiter, MiddlewareLimiter] 11 | ) -> Tuple[List[Depends], List[DispatchFunction]]: 12 | 13 | dispatch_functions: List[DispatchFunction] = [] 14 | dependencies: List[Depends] = [] 15 | 16 | for limiter in limiters: 17 | if isinstance(limiter, MiddlewareLimiter): 18 | dispatch_functions.append(limiter.dispatch) 19 | elif isinstance(limiter, FastAPILimiter): 20 | dependencies.append(Depends(limiter)) 21 | else: 22 | raise TypeError(f"Object {limiter} is not a Middleware or Limiter.") 23 | return dependencies, dispatch_functions 24 | -------------------------------------------------------------------------------- /.github/workflows/contribute.yml: -------------------------------------------------------------------------------- 1 | name: Contributing checks 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | checks: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-python@v2 16 | - name: Formatting (pre-commit) 17 | uses: pre-commit/action@v2.0.3 18 | - name: Setup poetry 19 | run: | 20 | pip install --upgrade pip 21 | pip install poetry 22 | - name: Install dependencies 23 | run: poetry install 24 | - name: Type checking 25 | run: poetry run mypy . 26 | - name: Linting 27 | run: | 28 | poetry run pylint --rcfile=.pylint.cfg throttled 29 | poetry run pylint --rcfile=.pylint.cfg --disable=redefined-outer-name tests 30 | - name: Testing 31 | run: poetry run pytest --cov=throttled --cov-report=xml tests 32 | - name: SonarCloud Scan 33 | uses: SonarSource/sonarcloud-github-action@master 34 | env: 35 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vinícius Vargas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /throttled/storage/_abstract.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from throttled.models import Hit 4 | 5 | 6 | class _HitsWindow(ABC): 7 | @abstractmethod 8 | def get_remaining_seconds(self) -> float: 9 | """Get the time (seconds) in which this HitsWindow will expire.""" 10 | 11 | @abstractmethod 12 | def incr(self, hits: int = 1) -> int: 13 | """ 14 | Increase the hit count in this window 15 | 16 | :param hits: the number of hits to increase in the window 17 | :return: the current hits number, after increased 18 | """ 19 | 20 | def is_expired(self) -> bool: 21 | """ 22 | Check if this window already expired 23 | 24 | :return: boolean True if the window already expired, False otherwise 25 | """ 26 | return self.get_remaining_seconds() <= 0 27 | 28 | 29 | class _WindowRepository(ABC): 30 | @abstractmethod 31 | def get_active_window(self, hit: Hit) -> _HitsWindow: 32 | """ 33 | Get the current active HitsWindow 34 | 35 | :param hit: hit to get the HitsWindow for 36 | :return: the currently active HitsWindow instance 37 | """ 38 | -------------------------------------------------------------------------------- /tests/unit/decorator_test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from tests.utils_for_test import NumbersComparer 6 | from throttled.exceptions import RateLimitExceeded 7 | from throttled.strategies import Strategies 8 | 9 | numbers_almost_equals = NumbersComparer(error=1e-2).almost_equals 10 | 11 | 12 | def function() -> bool: 13 | """Boilerplate function to test the limiter decorator.""" 14 | return True 15 | 16 | 17 | async def coroutine_function() -> bool: 18 | """Boilerplate coroutine function to test the limiter decorator.""" 19 | return True 20 | 21 | 22 | def test_limited_function(limit, limiter_for): 23 | limiter = limiter_for(Strategies.MOVING_WINDOW) 24 | limited_func = limiter.decorate(function) 25 | 26 | ret = [limited_func() for _ in range(limit.hits)] 27 | 28 | with pytest.raises(RateLimitExceeded) as err: 29 | limited_func() 30 | 31 | assert all(ret) 32 | assert numbers_almost_equals(err.value.retry_after, limit.interval) 33 | 34 | 35 | async def test_limited_coroutine_function(limit, limiter_for): 36 | limiter = limiter_for(Strategies.MOVING_WINDOW) 37 | limited_coroutine_func = limiter.decorate(coroutine_function) 38 | 39 | ret = await asyncio.gather(*(limited_coroutine_func() for _ in range(limit.hits))) 40 | 41 | with pytest.raises(RateLimitExceeded) as err: 42 | await limited_coroutine_func() 43 | 44 | assert all(ret) 45 | assert numbers_almost_equals(err.value.retry_after, limit.interval) 46 | -------------------------------------------------------------------------------- /tests/utils_for_test.py: -------------------------------------------------------------------------------- 1 | from math import tanh 2 | from pkgutil import ModuleInfo, walk_packages 3 | from types import ModuleType 4 | from typing import Iterable, Optional, Sequence 5 | 6 | 7 | def get_packages_in_module(module: ModuleType) -> Iterable[ModuleInfo]: 8 | return walk_packages(module.__path__, prefix=module.__name__ + ".") 9 | 10 | 11 | def get_package_paths_in_module(module: ModuleType) -> Sequence[str]: 12 | return [package.name for package in get_packages_in_module(module)] 13 | 14 | 15 | class NumbersComparer: 16 | """ 17 | A simple class for comparing numbers, given an error 18 | """ 19 | 20 | __slots__ = ("__error",) 21 | 22 | def __init__(self, error: float = 1e-2, interval: Optional[float] = None): 23 | self.__error = error 24 | if interval is not None: 25 | self.__error = interval * (0.15 - 0.10 * tanh(interval)) 26 | 27 | def almost_equals(self, retry_after: float, interval: float) -> bool: 28 | """Checks if two numbers are almost equal, given an error.""" 29 | return abs(retry_after - interval) <= self.__error 30 | 31 | 32 | NOT_SET = object() 33 | 34 | 35 | class Context: 36 | """ 37 | A simple class for storing a test context. 38 | """ 39 | 40 | def __init__(self, **kwargs): 41 | self.__dict__.update(kwargs) 42 | 43 | def __getattribute__(self, item): 44 | try: 45 | return super().__getattribute__(item) 46 | except AttributeError: 47 | return NOT_SET 48 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import fakeredis 2 | import pytest 3 | 4 | from tests.utils_for_test import Context, NumbersComparer 5 | from throttled.limiter import Limiter 6 | from throttled.models import Rate 7 | from throttled.storage import BaseStorage 8 | from throttled.storage.memory import MemoryStorage 9 | from throttled.storage.redis import RedisStorage 10 | from throttled.strategies import Strategies 11 | 12 | 13 | def redis() -> RedisStorage: 14 | return RedisStorage(client=fakeredis.FakeRedis()) 15 | 16 | 17 | @pytest.fixture(params=[MemoryStorage, redis]) 18 | def storage(request) -> BaseStorage: 19 | return request.param() 20 | 21 | 22 | @pytest.fixture(params=[Rate(2, 1), Rate(1, 0.1), Rate(5, 2)]) 23 | def limit(request) -> Rate: 24 | return request.param 25 | 26 | 27 | @pytest.fixture 28 | def limiter_for(storage, limit): 29 | def fac( 30 | sttg: Strategies, lim: Rate = limit, strg: BaseStorage = storage 31 | ) -> Limiter: 32 | return Limiter(strategy=sttg, storage=strg, limit=lim) 33 | 34 | return fac 35 | 36 | 37 | @pytest.fixture(params=Strategies) 38 | def limiter(limiter_for, request): 39 | return limiter_for(request.param) 40 | 41 | 42 | @pytest.fixture 43 | def context(): 44 | return Context() 45 | 46 | 47 | @pytest.fixture 48 | def comparer(limit) -> NumbersComparer: 49 | """ 50 | This comparer implements error tolerance for comparing numbers in steps. 51 | Tolerance: from 5% for larger intervals to 15% for smaller intervals. 52 | 53 | :return: Error tolerance when comparing numbers 54 | """ 55 | return NumbersComparer(interval=limit.interval) 56 | -------------------------------------------------------------------------------- /throttled/limiter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | from typing import Any, Callable, TypeVar, cast 4 | 5 | from throttled.exceptions import RateLimitExceeded 6 | from throttled.models import Hit, Rate 7 | from throttled.storage import BaseStorage 8 | from throttled.strategies import Strategies 9 | 10 | FuncT = TypeVar("FuncT", bound=Callable[..., Any]) 11 | 12 | 13 | class Limiter: 14 | __slots__ = ("__limit", "__windows_repository") 15 | 16 | def __init__( 17 | self, 18 | limit: Rate, 19 | storage: BaseStorage, 20 | strategy: Strategies, 21 | ): 22 | self.__limit = limit 23 | self.__windows_repository = storage.get_window_repository( 24 | strategy=strategy, limit=limit 25 | ) 26 | 27 | def __maybe_block(self, hit: Hit): 28 | """ 29 | :param hit: The hit to be tested 30 | :raises: RateLimitExceeded 31 | """ 32 | window = self.__windows_repository.get_active_window(hit) 33 | if window.incr(hit.cost) > self.__limit.hits: 34 | raise RateLimitExceeded( 35 | hit.key, 36 | retry_after=window.get_remaining_seconds(), 37 | ) 38 | 39 | def limit(self, key: str): 40 | self.__maybe_block(Hit(key=key)) 41 | 42 | def decorate(self, func: FuncT) -> FuncT: 43 | key = f"func={func.__name__}:hash={hash(func)}" 44 | 45 | @functools.wraps(func) 46 | def wrapper(*args, **kwargs): 47 | self.limit(key) 48 | return func(*args, **kwargs) 49 | 50 | @functools.wraps(func) 51 | async def a_wrapper(*args, **kwargs): 52 | self.limit(key) 53 | return await func(*args, **kwargs) 54 | 55 | return cast(FuncT, a_wrapper if asyncio.iscoroutinefunction(func) else wrapper) 56 | -------------------------------------------------------------------------------- /tests/unit/fixed_window_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fixed window rate limiter is very hard to test because we can't control 3 | at what time we are hitting the limited resources. 4 | 5 | Therefore, we are only testing the retry_after parameter is lesser than 6 | the interval for the window. 7 | """ 8 | 9 | import asyncio 10 | import time 11 | 12 | import pytest 13 | 14 | from throttled.exceptions import RateLimitExceeded 15 | from throttled.strategies import Strategies 16 | 17 | 18 | def time_in_window(limit) -> float: 19 | return time.time() % limit.interval 20 | 21 | 22 | @pytest.fixture 23 | def key() -> str: 24 | return "my-custom-key" 25 | 26 | 27 | def time_to_window_begin(limit): 28 | imprecision = limit.interval * 1e-5 29 | return limit.interval - time_in_window(limit) + imprecision 30 | 31 | 32 | async def test_hit_fixed_window_at_begin(key, limit, limiter_for): 33 | """ 34 | Hitting a fixed window at begin works the same way as first hitting a moving window. 35 | """ 36 | limiter = limiter_for(Strategies.FIXED_WINDOW) 37 | await asyncio.sleep(time_to_window_begin(limit)) 38 | 39 | for _ in range(limit.hits): 40 | limiter.limit(key) 41 | 42 | with pytest.raises(RateLimitExceeded) as err: 43 | limiter.limit(key) 44 | 45 | assert err.value.retry_after < limit.interval 46 | 47 | 48 | def time_to_half_window(limit): 49 | tiw = time_in_window(limit) 50 | if tiw > limit.interval / 2: 51 | return limit.interval / 2 + tiw 52 | return limit.interval / 2 - tiw 53 | 54 | 55 | async def test_hit_fixed_window_at_half(key, limit, limiter_for): 56 | limiter = limiter_for(Strategies.FIXED_WINDOW) 57 | await asyncio.sleep(time_to_half_window(limit)) 58 | 59 | for _ in range(limit.hits): 60 | limiter.limit(key) 61 | 62 | with pytest.raises(RateLimitExceeded) as err: 63 | limiter.limit(key) 64 | 65 | assert err.value.retry_after < limit.interval 66 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Test and Publish the package 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | tags: 9 | - '*.*.*' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | checks: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-python@v2 18 | - name: Formatting (pre-commit) 19 | uses: pre-commit/action@v2.0.3 20 | - name: Setup poetry 21 | run: | 22 | pip install --upgrade pip 23 | pip install poetry 24 | - name: Install dependencies 25 | run: poetry install 26 | - name: Type checking 27 | run: poetry run mypy . 28 | - name: Linting 29 | run: | 30 | poetry run pylint --rcfile=.pylint.cfg throttled 31 | poetry run pylint --rcfile=.pylint.cfg --disable=redefined-outer-name tests 32 | 33 | tests: 34 | needs: 35 | - checks 36 | strategy: 37 | matrix: 38 | python_version: [3.7, 3.8, 3.9] 39 | fastapi_version: [0.65, 0.66, 0.67, 0.68, 0.69, '0.70', 0.71] 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v2 43 | - uses: actions/setup-python@v2 44 | with: 45 | python-version: ${{matrix.python_version}} 46 | - name: Setup poetry 47 | run: | 48 | pip install --upgrade pip 49 | pip install poetry 50 | - name: Install dependencies 51 | run: | 52 | poetry install 53 | poetry run pip install fastapi==${{matrix.fastapi_version}} 54 | - name: Testing 55 | run: poetry run pytest tests 56 | 57 | deploy: 58 | needs: 59 | - tests 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v2 63 | - uses: actions/setup-python@v2 64 | - name: Setup poetry 65 | run: | 66 | pip install --upgrade pip 67 | pip install poetry 68 | - name: Configure Token 69 | run: poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }} 70 | - name: Build and Publish 71 | run: | 72 | poetry build 73 | poetry publish 74 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | 3 | - repo: https://github.com/Lucas-C/pre-commit-hooks 4 | rev: v1.1.13 5 | hooks: 6 | - id: remove-crlf 7 | 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v4.2.0 10 | hooks: 11 | - id: check-yaml 12 | - id: check-toml 13 | - id: debug-statements 14 | - id: detect-private-key 15 | - id: end-of-file-fixer 16 | - id: name-tests-test 17 | exclude: ^tests/acceptance/steps 18 | - id: sort-simple-yaml 19 | 20 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 21 | rev: v2.3.0 22 | hooks: 23 | - id: pretty-format-toml 24 | args: [--autofix] 25 | - id: pretty-format-yaml 26 | args: [--autofix, --indent, '1'] 27 | 28 | - repo: https://github.com/commitizen-tools/commitizen 29 | rev: v2.24.0 30 | hooks: 31 | - id: commitizen 32 | stages: [commit-msg] 33 | 34 | - repo: https://github.com/myint/autoflake 35 | rev: v1.4 36 | hooks: 37 | - id: autoflake 38 | name: 'autoflake: removes unused variables and imports.' 39 | entry: bash -c 'autoflake "$@"; git add -u' -- 40 | language: python 41 | args: [--in-place, --remove-all-unused-imports, --remove-unused-variables, --expand-star-imports, --ignore-init-module-imports] 42 | files: \.py$ 43 | stages: 44 | - merge-commit 45 | 46 | - repo: https://github.com/PyCQA/isort 47 | rev: 5.10.1 48 | hooks: 49 | - id: isort 50 | name: 'isort: sorts import statements.' 51 | entry: bash -c 'isort "$@"; git add -u' -- 52 | language: python 53 | args: [--filter-files] 54 | files: \.py$ 55 | 56 | - repo: https://github.com/psf/black 57 | rev: 22.3.0 58 | hooks: 59 | - id: black 60 | name: 'black: formats python code according to standards.' 61 | entry: bash -c 'black "$@"; git add -u' -- 62 | language: python 63 | files: \.py$ 64 | 65 | - repo: https://github.com/PyCQA/flake8 66 | rev: 4.0.1 67 | hooks: 68 | - id: flake8 69 | name: 'flake8: combines PEP8 style checks and pyflakes for error checking.' 70 | args: [--max-line-length, '120', --exclude, resources/write_readme.py] 71 | -------------------------------------------------------------------------------- /throttled/storage/memory.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines in-memory storage for WindowManager's 3 | """ 4 | 5 | 6 | import time 7 | from typing import MutableMapping, Optional 8 | 9 | from throttled.models import Hit, Rate 10 | from throttled.storage import BaseStorage 11 | from throttled.storage._abstract import _HitsWindow, _WindowRepository 12 | from throttled.storage._duration import DUR_REGISTRY, DurationCalcType 13 | from throttled.strategies import Strategies 14 | 15 | 16 | class _MemoryWindow(_HitsWindow): 17 | __slots__ = ("__expire_at", "__hits") 18 | 19 | def __init__(self, duration: float): 20 | self.__expire_at = time.time() + duration 21 | self.__hits = 0 22 | 23 | def incr(self, hits: int = 1) -> int: 24 | self.__hits += hits 25 | return self.__hits 26 | 27 | def get_remaining_seconds(self) -> float: 28 | return self.__expire_at - time.time() 29 | 30 | 31 | class _MemoryWindowRepository(_WindowRepository): 32 | __slots__ = ("__interval", "__cache", "__duration_calc") 33 | 34 | def __init__( 35 | self, 36 | interval: float, 37 | duration_func: DurationCalcType, 38 | cache: Optional[MutableMapping[str, _MemoryWindow]] = None, 39 | ): 40 | self.__cache: MutableMapping[str, _MemoryWindow] = ( 41 | cache if cache is not None else {} 42 | ) 43 | self.__interval = interval 44 | self.__duration_calc = duration_func 45 | 46 | def get_active_window(self, hit: Hit) -> _MemoryWindow: 47 | window = self.__cache.get(hit.key) 48 | if window is None or window.is_expired(): 49 | self.__cache[hit.key] = window = _MemoryWindow( 50 | duration=self.__duration_calc(hit.time, self.__interval) 51 | ) 52 | return window 53 | 54 | 55 | class MemoryStorage(BaseStorage): 56 | def __init__( 57 | self, 58 | cache: Optional[MutableMapping[str, _MemoryWindow]] = None, 59 | ): 60 | self.__cache = cache 61 | 62 | def get_window_repository( 63 | self, strategy: Strategies, limit: Rate 64 | ) -> _MemoryWindowRepository: 65 | return _MemoryWindowRepository( 66 | interval=limit.interval, 67 | duration_func=DUR_REGISTRY[strategy], 68 | cache=self.__cache, 69 | ) 70 | 71 | 72 | __all__ = ["MemoryStorage"] 73 | -------------------------------------------------------------------------------- /tests/unit/fastapi_utils_test.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import time 3 | from typing import List, Sequence 4 | 5 | import pytest 6 | from fastapi import Depends 7 | from starlette.requests import Request 8 | 9 | from throttled.fastapi import ( 10 | FastAPILimiter, 11 | IPLimiter, 12 | TotalLimiter, 13 | split_dependencies_and_middlewares, 14 | ) 15 | from throttled.models import Rate 16 | from throttled.storage import BaseStorage 17 | from throttled.strategies import Strategies 18 | 19 | 20 | class DependencyLimiter(FastAPILimiter): 21 | def __call__(self, request: Request, current_time=Depends(time.time)): 22 | self.limit(str(current_time)) 23 | 24 | 25 | def given_all_limiters(limit: Rate, storage: BaseStorage) -> List[FastAPILimiter]: 26 | return [ 27 | DependencyLimiter( 28 | limit=limit, storage=storage, strategy=Strategies.MOVING_WINDOW 29 | ), 30 | IPLimiter(limit=limit, storage=storage, strategy=Strategies.MOVING_WINDOW), 31 | TotalLimiter(limit=limit, storage=storage, strategy=Strategies.MOVING_WINDOW), 32 | ] 33 | 34 | 35 | def fastapi_dependencies_are_the_same( 36 | dependencies_1: Sequence[Depends], dependencies_2: Sequence[Depends] 37 | ) -> bool: 38 | for dep_1, dep_2 in itertools.zip_longest(dependencies_1, dependencies_2): 39 | if dep_1.dependency is not dep_2.dependency: 40 | return False 41 | return True 42 | 43 | 44 | def test_should_split_dependencies_and_middlewares(limit, storage): 45 | # Given 2 middleware limiters and 1 limiter as dependency 46 | limiters = given_all_limiters(limit, storage) 47 | dependency_limiter, ip_limiter, total_limiter = limiters 48 | 49 | # When spliting dependencies and middlewares 50 | dependencies, dispatch_functions = split_dependencies_and_middlewares(*limiters) 51 | 52 | # Then it should return only limiter as dependency 53 | assert fastapi_dependencies_are_the_same( 54 | dependencies, [Depends(dependency_limiter)] 55 | ) 56 | 57 | # And the other two limiters dispatch method as dispatch functions 58 | assert dispatch_functions == [ip_limiter.dispatch, total_limiter.dispatch] 59 | 60 | 61 | def test_should_raise_if_not_callable_limiter(): 62 | obj = object() 63 | with pytest.raises(TypeError) as err: 64 | _ = split_dependencies_and_middlewares(obj) 65 | 66 | assert err.value.args[0] == f"Object {obj} is not a Middleware or Limiter." 67 | -------------------------------------------------------------------------------- /throttled/storage/redis.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines redis storage for WindowManager's 3 | """ 4 | 5 | from redis.client import Redis 6 | 7 | from throttled.models import Hit, Rate 8 | from throttled.storage import BaseStorage 9 | from throttled.storage._abstract import _HitsWindow, _WindowRepository 10 | from throttled.storage._duration import DUR_REGISTRY, DurationCalcType 11 | from throttled.strategies import Strategies 12 | 13 | 14 | class _RedisWindow(_HitsWindow): 15 | __slots__ = ("__client", "__interval", "__hit", "__duration_func") 16 | 17 | def __init__( 18 | self, interval: float, client: Redis, hit: Hit, duration_func: DurationCalcType 19 | ): 20 | self.__client = client 21 | self.__interval = interval 22 | self.__duration_func = duration_func 23 | self.__hit = hit 24 | 25 | def incr(self, hits: int = 1) -> int: 26 | value = self.__client.incrby(name=self.__hit.key, amount=hits) 27 | if value == hits: 28 | self.__client.pexpire( 29 | name=self.__hit.key, 30 | time=int(self.__duration_func(self.__hit.time, self.__interval) * 1e3), 31 | ) 32 | return value 33 | 34 | def get_remaining_seconds(self) -> float: 35 | return self.__client.pttl(name=self.__hit.key) * 1e-3 36 | 37 | 38 | class _RedisWindowRepository(_WindowRepository): 39 | __slots__ = ("__interval", "__client", "__duration_func") 40 | 41 | def __init__(self, interval: float, client: Redis, duration_func: DurationCalcType): 42 | self.__client = client 43 | self.__interval = interval 44 | self.__duration_func = duration_func 45 | 46 | def get_active_window(self, hit: Hit) -> _RedisWindow: 47 | return _RedisWindow( 48 | client=self.__client, 49 | interval=self.__interval, 50 | hit=hit, 51 | duration_func=self.__duration_func, 52 | ) 53 | 54 | 55 | class RedisStorage(BaseStorage): 56 | def __init__( 57 | self, 58 | client: Redis, 59 | ): 60 | self.__client = client 61 | 62 | def get_window_repository( 63 | self, strategy: Strategies, limit: Rate 64 | ) -> _RedisWindowRepository: 65 | return _RedisWindowRepository( 66 | interval=limit.interval, 67 | duration_func=DUR_REGISTRY[strategy], 68 | client=self.__client, 69 | ) 70 | 71 | 72 | __all__ = ["RedisStorage"] 73 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.commitizen] 6 | version = "0.2.1" 7 | tag_format = "$major.$minor.$patch$prerelease" 8 | update_changelog_on_bump = true 9 | annotated_tag = true 10 | check_consistency = true 11 | version_files = ["VERSION", "pyproject.toml", "throttled/__init__.py",] 12 | 13 | [tool.mypy] 14 | ignore_missing_imports = true 15 | no_silence_site_packages = true 16 | disallow_untyped_calls = true 17 | no_implicit_optional = true 18 | warn_redundant_casts = true 19 | warn_unused_ignores = true 20 | disallow_untyped_globals = true 21 | disallow_redefinition = true 22 | no_implicit_reexport = true 23 | pretty = true 24 | sqlite_cache = true 25 | no_site_packages = true 26 | 27 | [[tool.mypy.overrides]] 28 | module = ["faker.*", "dotenv.*", "redis.*"] 29 | follow_imports = "skip" 30 | warn_unused_ignores = true 31 | 32 | [tool.poetry] 33 | name = "throttled" 34 | version = "0.2.1" 35 | description = "A rate limiter for FastAPI" 36 | authors = ["Vinícius Vargas "] 37 | readme = "README.md" 38 | homepage = "https://github.com/santunioni/ThrottledAPI" 39 | license = "MIT" 40 | repository = "https://github.com/santunioni/ThrottledAPI" 41 | include = ["throttled/py.typed", "LICENSE"] 42 | 43 | [tool.poetry.dependencies] 44 | python = "^3.7" 45 | requests = "^2.27.1" 46 | 47 | [tool.poetry.dev-dependencies] 48 | pytest = "^6.2.5" 49 | pytest-cov = "^3.0.0" 50 | pytest-randomly = "^3.11.0" 51 | pytest-asyncio = "^0.17.2" 52 | tox = "^3.24.5" 53 | mypy = "^0.931" 54 | pylint = "^2.12.2" 55 | commitizen = "^2.20.4" 56 | black = "^22.1" 57 | isort = "^5.10.1" 58 | pre-commit = "^2.16.0" 59 | devtools = "^0.8.0" 60 | fastapi = "^0.72.0" 61 | redis = "^4.0.0" 62 | pytest-pylint = "^0.18.0" 63 | pylint-pytest = "^1.1.2" 64 | fakeredis = "^1.7.0" 65 | pytest-bdd = "^5.0.0" 66 | pytest-watch = "^4.2.0" 67 | assertpy = "^1.1" 68 | 69 | [tool.poetry.extras] 70 | redis = ["redis"] 71 | fastapi = ["fastapi"] 72 | 73 | [tool.pytest.ini_options] 74 | asyncio_mode = "auto" 75 | python_files = ["*test*.py", "*should*.py"] 76 | python_functions = ["test_*", "should_*"] 77 | 78 | [tool.tox] 79 | legacy_tox_ini = """ 80 | [tox] 81 | isolated_build = True 82 | envlist = py37,py38,py39,py310 83 | [tox:.package] 84 | basepython = python3 85 | 86 | [testenv] 87 | deps = pytest 88 | pytest-randomly 89 | pytest-cov 90 | pytest-bdd 91 | pytest-asyncio 92 | redis 93 | fakeredis 94 | fastapi 95 | requests 96 | commands = pytest tests""" 97 | -------------------------------------------------------------------------------- /tests/acceptance/steps/fastapi.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi import FastAPI 3 | from pytest_bdd import given, parsers, then, when 4 | from starlette import status 5 | from starlette.middleware.base import BaseHTTPMiddleware 6 | from starlette.testclient import TestClient 7 | 8 | from tests.utils_for_test import NOT_SET, NumbersComparer 9 | from throttled.fastapi import IPLimiter, TotalLimiter 10 | from throttled.fastapi.base import key_detail_factory 11 | from throttled.models import Rate 12 | from throttled.storage.memory import MemoryStorage 13 | from throttled.strategies import Strategies 14 | 15 | 16 | @pytest.fixture 17 | def client(app): 18 | return TestClient(app) 19 | 20 | 21 | @given(parsers.parse("A Rest API built with FastAPI"), target_fixture="app") 22 | def app(): 23 | api = FastAPI() 24 | 25 | @api.get("/{path}") 26 | def route(path: str): 27 | return f"Hello {path}" 28 | 29 | return api 30 | 31 | 32 | @given( 33 | parsers.parse( 34 | "The API is limited {limiter_type} to {number} requests per {seconds} seconds" 35 | ) 36 | ) 37 | def build_limiter(app, limiter_type, number, seconds, context): 38 | number = int(number) 39 | seconds = float(seconds) 40 | limiter = {"by IP": IPLimiter, "globally": TotalLimiter}[limiter_type]( 41 | strategy=Strategies.MOVING_WINDOW, 42 | storage=MemoryStorage(), 43 | limit=Rate(number, seconds), 44 | detail_factory=key_detail_factory, 45 | ) 46 | 47 | app.add_middleware( 48 | BaseHTTPMiddleware, 49 | dispatch=limiter.dispatch, 50 | ) 51 | 52 | if context.limiters is NOT_SET: 53 | context.limiters = [] 54 | 55 | context.limiters.append(limiter) 56 | 57 | 58 | @when(parsers.parse("I hit the API {number} times in a row")) 59 | def hit_limiter(client, number, context): 60 | number = int(number) 61 | context.responses = [] 62 | for _ in range(number): 63 | context.responses.append(client.get("/there")) 64 | 65 | 66 | @then("All responses are successful") 67 | def check_responses(context): 68 | assert all(r.ok for r in context.responses) 69 | 70 | 71 | @then("The next hit is blocked by the limiter") 72 | def blocked_request(client, context): 73 | response = client.get("/there") 74 | assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS 75 | 76 | context.blocked = response 77 | 78 | 79 | @then(parsers.parse("I should only retry after {seconds} seconds")) 80 | def retry_after(context, seconds): 81 | seconds = float(seconds) 82 | 83 | retry_after_seconds = float(context.blocked.headers["retry-after"]) 84 | assert NumbersComparer(interval=seconds).almost_equals(retry_after_seconds, seconds) 85 | 86 | 87 | @given("The limiter ignores the path being tested") 88 | def ignore_path(context): 89 | for limiter in context.limiters: 90 | limiter.ignore_path("/there") 91 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.1 (2022-02-20) 2 | 3 | ### Fix 4 | 5 | - use ip fetching already implemented in Starlette 6 | 7 | ## 0.2.0 (2022-02-17) 8 | 9 | ## 0.1.16 (2022-02-17) 10 | 11 | ## 0.1.16a37 (2022-02-17) 12 | 13 | ## 0.1.16a36 (2022-02-12) 14 | 15 | ## 0.1.16a35 (2022-02-12) 16 | 17 | ## 0.1.16a34 (2022-02-12) 18 | 19 | ## 0.1.16a33 (2022-02-12) 20 | 21 | ## 0.1.16a32 (2022-02-12) 22 | 23 | ## 0.1.16a31 (2022-02-12) 24 | 25 | ## 0.1.16a30 (2022-02-12) 26 | 27 | ## 0.1.16a29 (2022-02-12) 28 | 29 | ### Refactor 30 | 31 | - make strategies a Enum 32 | 33 | ### Feat 34 | 35 | - implement strategies with redis storage 36 | - implement redis storage 37 | - add decr method to MemoryWindow 38 | 39 | ## 0.1.16a28 (2022-02-08) 40 | 41 | ## 0.1.16a27 (2022-02-08) 42 | 43 | ## 0.1.16a26 (2022-02-07) 44 | 45 | ## 0.1.16a25 (2022-02-07) 46 | 47 | ### Fix 48 | 49 | - cast retry-after http header 50 | - window duration in fixed window strategy for memory storage 51 | 52 | ## 0.1.16a24 (2022-02-07) 53 | 54 | ### Fix 55 | 56 | - add cast to str in exc.retry_after 57 | 58 | ## 0.1.16a23 (2022-02-07) 59 | 60 | ### Fix 61 | 62 | - check if FastAPILimiter instead of Limiter 63 | 64 | ## 0.1.16a21 (2022-02-07) 65 | 66 | ## 0.1.16a20 (2022-02-03) 67 | 68 | ## 0.1.16a19 (2022-02-03) 69 | 70 | ### Refactor 71 | 72 | - fastapi limiters 73 | 74 | ## 0.1.16a18 (2022-01-30) 75 | 76 | ### Feat 77 | 78 | - implement moving window for memory storage 79 | 80 | ### Perf 81 | 82 | - optimize fixed window strategy 83 | 84 | ## 0.1.16a17 (2022-01-25) 85 | 86 | ## 0.1.16a16 (2022-01-25) 87 | 88 | ## 0.1.16a15 (2022-01-24) 89 | 90 | ### Refactor 91 | 92 | - fastapi module 93 | 94 | ## 0.1.16a14 (2022-01-23) 95 | 96 | ### Fix 97 | 98 | - remove exception being raised improperty in fastapi.builter 99 | 100 | ## 0.1.16a13 (2022-01-23) 101 | 102 | ### Fix 103 | 104 | - implement abstract method 105 | 106 | ## 0.1.16a12 (2022-01-23) 107 | 108 | ### Refactor 109 | 110 | - extract Limiter 111 | 112 | ## 0.1.16a11 (2022-01-23) 113 | 114 | ### Fix 115 | 116 | - wrong import 117 | 118 | ## 0.1.16a10 (2022-01-23) 119 | 120 | ## 0.1.16a9 (2022-01-23) 121 | 122 | ### Fix 123 | 124 | - window strategy 125 | 126 | ## 0.1.16a8 (2022-01-23) 127 | 128 | ### Fix 129 | 130 | - middleware 131 | 132 | ## 0.1.16a7 (2022-01-23) 133 | 134 | ## 0.1.16a6 (2022-01-22) 135 | 136 | ## 0.1.16a5 (2022-01-22) 137 | 138 | ### Fix 139 | 140 | - remove updating dependencies after creating app 141 | 142 | ## 0.1.16a4 (2022-01-22) 143 | 144 | ### Feat 145 | 146 | - add ignored paths to middleware 147 | 148 | ## 0.1.16a3 (2022-01-22) 149 | 150 | ## 0.1.16a2 (2022-01-22) 151 | 152 | ### Perf 153 | 154 | - add middlewares for better performance 155 | 156 | ## 0.1.16a1 (2022-01-22) 157 | 158 | ### Fix 159 | 160 | - import time in models 161 | - import time in models 162 | 163 | ## 0.1.16a0 (2022-01-22) 164 | 165 | ## 0.1.15 (2022-01-22) 166 | 167 | ### Fix 168 | 169 | - change precision to microseconds 170 | 171 | ## 0.1.14 (2022-01-22) 172 | 173 | ## 0.1.13 (2022-01-22) 174 | 175 | ## 0.1.12 (2022-01-22) 176 | 177 | ## 0.1.11 (2022-01-22) 178 | 179 | ### Fix 180 | 181 | - wrong import 182 | 183 | ## 0.1.10 (2022-01-22) 184 | 185 | ### Feat 186 | 187 | - host based limiter 188 | 189 | ## 0.1.9 (2022-01-22) 190 | 191 | ### Feat 192 | 193 | - fixed window block based on key 194 | 195 | ## 0.1.8 (2022-01-22) 196 | 197 | ### Fix 198 | 199 | - wrong import 200 | 201 | ## 0.1.7 (2022-01-22) 202 | 203 | ## 0.1.6 (2022-01-22) 204 | 205 | ### Feat 206 | 207 | - add retry-after to handler 208 | 209 | ## 0.1.5 (2022-01-22) 210 | 211 | ### Feat 212 | 213 | - add retry-after 214 | 215 | ## 0.1.4 (2022-01-22) 216 | 217 | ### Feat 218 | 219 | - add handler for fastapi 220 | 221 | ## 0.1.3 (2022-01-22) 222 | 223 | ### Fix 224 | 225 | - wrong import 226 | 227 | ## 0.1.2 (2022-01-22) 228 | 229 | ### Feat 230 | 231 | - add simple limiter for fastapi 232 | 233 | ## 0.1.1 (2022-01-18) 234 | 235 | ### Perf 236 | 237 | - optimize Rate.create_from_hits for memory 238 | 239 | ### Feat 240 | 241 | - implement models 242 | -------------------------------------------------------------------------------- /throttled/fastapi/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Callable, Optional, Sequence 3 | 4 | from fastapi.exceptions import HTTPException 5 | from starlette import status 6 | from starlette.middleware.base import RequestResponseEndpoint 7 | from starlette.requests import Request 8 | from starlette.responses import Response 9 | 10 | from throttled.exceptions import RateLimitExceeded 11 | from throttled.limiter import Limiter 12 | from throttled.models import Rate 13 | from throttled.storage import BaseStorage 14 | from throttled.strategies import Strategies 15 | 16 | 17 | class HTTPLimitExceeded(HTTPException): 18 | def __init__(self, exc: RateLimitExceeded, detail: Optional[str] = None): 19 | self.retry_after = exc.retry_after 20 | super_kwargs = dict( 21 | status_code=status.HTTP_429_TOO_MANY_REQUESTS, 22 | headers={"Retry-After": str(round(self.retry_after, 2))}, 23 | ) 24 | if detail is not None: 25 | super_kwargs["detail"] = detail 26 | super().__init__(**super_kwargs) 27 | 28 | 29 | DetailFactory = Callable[[RateLimitExceeded], Optional[str]] 30 | 31 | 32 | def key_detail_factory(exc: RateLimitExceeded) -> str: 33 | return f"Rate exceeded for key={exc.key}." 34 | 35 | 36 | class FastAPILimiter(ABC): 37 | """First adapter between limiters APIs and FastAPI""" 38 | 39 | def __init__( 40 | self, 41 | limit: Rate, 42 | storage: BaseStorage, 43 | strategy: Strategies = Strategies.MOVING_WINDOW, 44 | detail_factory: DetailFactory = key_detail_factory, 45 | ): 46 | self.__limiter = Limiter(limit, storage, strategy) 47 | self.__detail_factory = detail_factory 48 | 49 | def limit(self, key: str): 50 | """ 51 | Limit the hit based on it's key 52 | 53 | :param key: the key to be limited. 54 | :raises: HTTPLimitExceeded 55 | """ 56 | try: 57 | self.__limiter.limit(key) 58 | except RateLimitExceeded as exc: 59 | raise HTTPLimitExceeded(exc, detail=self.__detail_factory(exc)) from exc 60 | 61 | @abstractmethod 62 | def __call__(self, request: Request): 63 | """ 64 | This method implementation are supposed to call the limit() method with 65 | the key to be limited. 66 | 67 | :param request: injected by FastAPI when using the limiter as dependency. 68 | :raises: HTTPLimitExceeded 69 | """ 70 | 71 | 72 | ResponseFactory = Callable[[HTTPLimitExceeded], Response] 73 | 74 | 75 | def default_response_factory(exc: HTTPLimitExceeded) -> Response: 76 | return Response( 77 | status_code=exc.status_code, 78 | headers=exc.headers, 79 | ) 80 | 81 | 82 | class MiddlewareLimiter(FastAPILimiter, ABC): 83 | __ignored_paths: Sequence[str] = ("docs", "redoc", "favicon.ico", "openapi.json") 84 | 85 | def __init__( 86 | self, 87 | limit: Rate, 88 | storage: BaseStorage, 89 | strategy: Strategies = Strategies.MOVING_WINDOW, 90 | detail_factory: DetailFactory = lambda exc: None, 91 | response_factory: ResponseFactory = default_response_factory, 92 | ): 93 | super().__init__( 94 | strategy=strategy, 95 | limit=limit, 96 | storage=storage, 97 | detail_factory=detail_factory, 98 | ) 99 | self.__response_factory = response_factory 100 | 101 | def ignore_path(self, path: str): 102 | path = path.lstrip("/") 103 | ignored = list(self.__ignored_paths) 104 | ignored.append(path) 105 | self.__ignored_paths = ignored 106 | 107 | async def dispatch( 108 | self, request: Request, call_next: RequestResponseEndpoint 109 | ) -> Response: 110 | """ 111 | Dispatch function which converts the limiter in a Middleware. 112 | Middlewares are faster because they don't need dependencies to be resolved. 113 | 114 | All limiters that don't use FastAPI dependency injection can be implemented as Middleware. 115 | 116 | :param request: injected by FastAPI when using the limiter as middleware. 117 | :param call_next: the next middleware from the chain-of-responsibility created by Starlette. 118 | """ 119 | try: 120 | path = str(request.url).replace(str(request.base_url), "") 121 | if path not in self.__ignored_paths: 122 | self(request) 123 | return await call_next(request) 124 | except HTTPLimitExceeded as exc: 125 | return self.__response_factory(exc) 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThrottledAPI 2 | 3 | ThrottledAPI is a rate limiter for FastAPI. 4 | Check [our features](tests/acceptance/features/fastapi_limiter.feature) to see the use-cases already tested. The code base is 99% covered by unit and integration tests. It is also full type checked with type hints, assuring code with great quality. 5 | 6 | ## Why another rate limiter for FastAPI? 7 | 8 | Why another rate limiter for FastAPI, if we already have 9 | [slowapi](https://github.com/laurentS/slowapi) and 10 | [fastapi-limiter](https://github.com/long2ice/fastapi-limiter)? This limiter glues what is good from both projects and 11 | adds a bit more. Here is a list of reasons: 12 | 13 | - The `throttled-api` rate limiter takes full advantage from the composable dependency injection system in FastAPI. 14 | That means you can also create limiters per resource. 15 | - Want to limit requests per IP or per user? Got it! 16 | - Want to limit requests based on another weird parameter you are receiving? Just extend our `FastAPILimiter` and you 17 | are good to go! 18 | - You can use different storage backends (different implementations for `BaseStorage`) for each limiter. 19 | - Want to limit each API instance to 2000 requests per second? You don´t need more than a *in-memory* counter. 20 | Just use `MemoryStorage` for the task. 21 | - Want to limit calls to all your API instances by user or IP? A shared cache is what you need. 22 | Our `RedisStorage` implementation is an adapter for the famous [redis](https://github.com/redis/redis-py) package. Other implementations + asyncio support are coming... 23 | 24 | ## Install 25 | 26 | Just use your favorite python package manager. Here are two examples: 27 | 28 | - With pip: `pip install throttled` 29 | - With poetry: `poetry add throttled` 30 | 31 | The package is in an early development stage. This means the API can change a lot along the way, before hitting 1.0.0, 32 | given community feedback. I recommend you pin the latest version that works for you when using the library for now. 33 | 34 | ## Use 35 | 36 | ### Use existing limiters 37 | 38 | We already implemented `TotalLimiter` and `IPLimiter` for you: 39 | 40 | - `TotalLimiter`: limits all calls to your API, so you can assure it won't suffocate with too many requests. 41 | - `IPLimiter`: as the name suggests, limits requests by IP. 42 | Disclaimer: generalize getting IP from the `Request` object is not easy, because there is [too many variations in the community](https://en.wikipedia.org/wiki/Talk:X-Forwarded-For#Variations). The `IPLimiter` works when hitting the API running on `uvicorn` from another docker container. However, reverse proxies can complicate things. Notwithstanding, the `ThrottledAPI` archtecture allows creating limiters as you like, so there is nothing stopping you from creating an IP limiter that fits your infrastructure. You can also submit it here as a PR, and I will be glad to merge it in the codebase, given instructions for when to use it. 43 | 44 | ### Implement custom limiters 45 | 46 | You can implement new limiters easily extending from `FastAPILimiter` or `MiddlewareLimiter` 47 | 48 | ```python 49 | # Your IDE will help you find the imports 50 | 51 | class UserLimiter(FastAPILimiter): 52 | """Client specific limiter""" 53 | 54 | def __call__(self, request: Request, user: UserID = Depends(get_current_user)): 55 | # The request parameter is mandatory 56 | self.limit(key=f"username={user.username}") 57 | ``` 58 | 59 | ### Attach to the API 60 | 61 | There are two options when using the limiters in your API 62 | 63 | #### All limiters as dependencies 64 | 65 | This is the simplest usage, requiring less code 66 | 67 | ```python 68 | def create_limiters() -> Sequence[FastAPILimiter]: 69 | memory = MemoryStorage(cache={}) 70 | api_limiter = TotalLimiter(limit=Rate(2000, 1), storage=memory) 71 | 72 | redis = RedisStorage(client=Redis.from_url("redis://localhost:0")) 73 | ip_limiter = IPLimiter(limit=Rate(10, 1), storage=redis) 74 | user_limiter = UserLimiter(limit=Rate(2, 5), storage=redis) 75 | 76 | return api_limiter, ip_limiter, user_limiter 77 | 78 | 79 | def create_app(limiters: Sequence[FastAPILimiter] = tuple()) -> FastAPI: 80 | """Creates a FastAPI app with attached limiters and routes""" 81 | api = FastAPI(title="Snacks bar", dependencies=limiters) 82 | 83 | api.include_router(products_router, prefix="/products") 84 | api.include_router(users_router, prefix="/users") 85 | return api 86 | 87 | 88 | app = create_app(limiters=create_limiters()) 89 | ``` 90 | 91 | #### Some limiters as middlewares 92 | 93 | Although FastAPI dependency injection is really powerful, some limiters doesn't require any special resource in 94 | other to do their job. In that case you cut some latency if using the limiter as a Middleware. 95 | 96 | ```python 97 | def create_app(limiters: Sequence[FastAPILimiter] = tuple()) -> FastAPI: 98 | """Creates a FastAPI app with attached limiters and routes""" 99 | dependencies, middlewares = split_dependencies_and_middlewares(*limiters) 100 | 101 | api = FastAPI(title="Snacks bar", dependencies=dependencies) 102 | 103 | api.include_router(products_router, prefix="/products") 104 | api.include_router(users_router, prefix="/users") 105 | 106 | for mid in middlewares: 107 | api.add_middleware(BaseHTTPMiddleware, dispatch=mid) 108 | 109 | return api 110 | 111 | 112 | app = create_app(limiters=create_limiters()) # create_limiter: same function above 113 | ``` 114 | 115 | ## Middleware vs Dependency 116 | 117 | When implementing a custom limiter, how to choose between extending `FastAPILimiter` or `MiddlewareLimiter`? 118 | 119 | ```mermaid 120 | stateDiagram-v2 121 | state FirstCondition <> 122 | state SecondCondition <> 123 | 124 | FirstQuestion: What type of limiter should I choose? 125 | FirstQuestion --> FirstCondition 126 | 127 | FirstCondition: Limiting depends on resources other\nthan Request object from Starlette? 128 | FirstCondition --> FastAPILimiter: yes 129 | FirstCondition --> MiddlewareLimiter : no 130 | FastAPILimiter --> SecondQuestion 131 | MiddlewareLimiter --> SecondQuestion 132 | 133 | SecondQuestion: What storage should I pick? 134 | SecondQuestion --> SecondCondition 135 | SecondCondition: The parameters you are limiting spams a parameter space.\n Is that space too large? 136 | SecondCondition --> RedisStorage : yes 137 | SecondCondition --> ThirdCondition : no 138 | 139 | ThirdCondition: You want to share the limiter\nbetween different API instances (pods)? 140 | ThirdCondition --> RedisStorage : yes 141 | ThirdCondition --> MemoryStorage : no 142 | 143 | RedisStorage --> End 144 | MemoryStorage --> End 145 | End: Attach the limiter to your API 146 | ``` 147 | 148 | ## Contributing 149 | 150 | Issues, suggestions, PRs are welcome! 151 | -------------------------------------------------------------------------------- /.pylint.cfg: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-allow-list= 7 | 8 | # A comma-separated list of package or module names from where C extensions may 9 | # be loaded. Extensions are loading into the active Python interpreter and may 10 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 11 | # for backward compatibility.) 12 | extension-pkg-whitelist= 13 | 14 | # Return non-zero exit code if any of these messages/categories are detected, 15 | # even if score is above --fail-under value. Syntax same as enable. Messages 16 | # specified are enabled, while categories only check already-enabled messages. 17 | fail-on= 18 | 19 | # Specify a score threshold to be exceeded before program exits with error. 20 | fail-under=10.0 21 | 22 | # Files or directories to be skipped. They should be base names, not paths. 23 | ignore=CVS 24 | 25 | # Add files or directories matching the regex patterns to the ignore-list. The 26 | # regex matches against paths and can be in Posix or Windows format. 27 | ignore-paths= 28 | 29 | # Files or directories matching the regex patterns are skipped. The regex 30 | # matches against base names, not paths. 31 | ignore-patterns= 32 | 33 | # Python code to execute, usually for sys.path manipulation such as 34 | # pygtk.require(). 35 | #init-hook= 36 | 37 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 38 | # number of processors available to use. 39 | jobs=1 40 | 41 | # Control the amount of potential inferred values when inferring a single 42 | # object. This can help the performance when dealing with large functions or 43 | # complex, nested conditions. 44 | limit-inference-results=100 45 | 46 | # List of plugins (as comma separated values of python module names) to load, 47 | # usually to register additional checkers. 48 | load-plugins=pylint_pytest 49 | 50 | # Pickle collected data for later comparisons. 51 | persistent=yes 52 | 53 | # Minimum Python version to use for version dependent checks. Will default to 54 | # the version used to run pylint. 55 | py-version=3.7 56 | 57 | # When enabled, pylint would attempt to guess common misconfiguration and emit 58 | # user-friendly hints instead of false-positive error messages. 59 | suggestion-mode=yes 60 | 61 | # Allow loading of arbitrary C extensions. Extensions are imported into the 62 | # active Python interpreter and may run arbitrary code. 63 | unsafe-load-any-extension=no 64 | 65 | 66 | [MESSAGES CONTROL] 67 | 68 | # Only show warnings with the listed confidence levels. Leave empty to show 69 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 70 | confidence= 71 | 72 | # Disable the message, report, category or checker with the given id(s). You 73 | # can either give multiple identifiers separated by comma (,) or put this 74 | # option multiple times (only on the command line, not in the configuration 75 | # file where it should appear only once). You can also use "--disable=all" to 76 | # disable everything first and then reenable specific checks. For example, if 77 | # you want to run only the similarities checker, you can use "--disable=all 78 | # --enable=similarities". If you want to run only the classes checker, but have 79 | # no Warning level messages displayed, use "--disable=all --enable=classes 80 | # --disable=W". 81 | disable=missing-module-docstring, 82 | missing-class-docstring, 83 | missing-function-docstring, 84 | line-too-long, 85 | no-name-in-module, 86 | raw-checker-failed, 87 | bad-inline-option, 88 | locally-disabled, 89 | file-ignored, 90 | suppressed-message, 91 | useless-suppression, 92 | deprecated-pragma, 93 | use-symbolic-message-instead, 94 | c-extension-no-member, 95 | too-few-public-methods, 96 | too-many-arguments, 97 | fixme, 98 | unused-argument 99 | 100 | # Enable the message, report, category or checker with the given id(s). You can 101 | # either give multiple identifier separated by comma (,) or put this option 102 | # multiple time (only on the command line, not in the configuration file where 103 | # it should appear only once). See also the "--disable" option for examples. 104 | enable= 105 | 106 | 107 | [REPORTS] 108 | 109 | # Python expression which should return a score less than or equal to 10. You 110 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 111 | # which contain the number of messages in each category, as well as 'statement' 112 | # which is the total number of statements analyzed. This score is used by the 113 | # global evaluation report (RP0004). 114 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 115 | 116 | # Template used to display messages. This is a python new-style format string 117 | # used to format the message information. See doc for all details. 118 | #msg-template= 119 | 120 | # Set the output format. Available formats are text, parseable, colorized, json 121 | # and msvs (visual studio). You can also give a reporter class, e.g. 122 | # mypackage.mymodule.MyReporterClass. 123 | output-format=text 124 | 125 | # Tells whether to display a full report or only the messages. 126 | reports=no 127 | 128 | # Activate the evaluation score. 129 | score=yes 130 | 131 | 132 | [REFACTORING] 133 | 134 | # Maximum number of nested blocks for function / method body 135 | max-nested-blocks=5 136 | 137 | # Complete name of functions that never returns. When checking for 138 | # inconsistent-return-statements if a never returning function is called then 139 | # it will be considered as an explicit return statement and no message will be 140 | # printed. 141 | never-returning-functions=sys.exit,argparse.parse_error 142 | 143 | 144 | [BASIC] 145 | 146 | # Naming style matching correct argument names. 147 | argument-naming-style=snake_case 148 | 149 | # Regular expression matching correct argument names. Overrides argument- 150 | # naming-style. 151 | #argument-rgx= 152 | 153 | # Naming style matching correct attribute names. 154 | attr-naming-style=snake_case 155 | 156 | # Regular expression matching correct attribute names. Overrides attr-naming- 157 | # style. 158 | #attr-rgx= 159 | 160 | # Bad variable names which should always be refused, separated by a comma. 161 | bad-names=foo, 162 | bar, 163 | baz, 164 | toto, 165 | tutu, 166 | tata 167 | 168 | # Bad variable names regexes, separated by a comma. If names match any regex, 169 | # they will always be refused 170 | bad-names-rgxs= 171 | 172 | # Naming style matching correct class attribute names. 173 | class-attribute-naming-style=any 174 | 175 | # Regular expression matching correct class attribute names. Overrides class- 176 | # attribute-naming-style. 177 | #class-attribute-rgx= 178 | 179 | # Naming style matching correct class constant names. 180 | class-const-naming-style=UPPER_CASE 181 | 182 | # Regular expression matching correct class constant names. Overrides class- 183 | # const-naming-style. 184 | #class-const-rgx= 185 | 186 | # Naming style matching correct class names. 187 | class-naming-style=PascalCase 188 | 189 | # Regular expression matching correct class names. Overrides class-naming- 190 | # style. 191 | #class-rgx= 192 | 193 | # Naming style matching correct constant names. 194 | const-naming-style=UPPER_CASE 195 | 196 | # Regular expression matching correct constant names. Overrides const-naming- 197 | # style. 198 | #const-rgx= 199 | 200 | # Minimum line length for functions/classes that require docstrings, shorter 201 | # ones are exempt. 202 | docstring-min-length=-1 203 | 204 | # Naming style matching correct function names. 205 | function-naming-style=snake_case 206 | 207 | # Regular expression matching correct function names. Overrides function- 208 | # naming-style. 209 | #function-rgx= 210 | 211 | # Good variable names which should always be accepted, separated by a comma. 212 | good-names=i, 213 | j, 214 | k, 215 | ex, 216 | Run, 217 | _ 218 | 219 | # Good variable names regexes, separated by a comma. If names match any regex, 220 | # they will always be accepted 221 | good-names-rgxs= 222 | 223 | # Include a hint for the correct naming format with invalid-name. 224 | include-naming-hint=no 225 | 226 | # Naming style matching correct inline iteration names. 227 | inlinevar-naming-style=any 228 | 229 | # Regular expression matching correct inline iteration names. Overrides 230 | # inlinevar-naming-style. 231 | #inlinevar-rgx= 232 | 233 | # Naming style matching correct method names. 234 | method-naming-style=snake_case 235 | 236 | # Regular expression matching correct method names. Overrides method-naming- 237 | # style. 238 | #method-rgx= 239 | 240 | # Naming style matching correct module names. 241 | module-naming-style=snake_case 242 | 243 | # Regular expression matching correct module names. Overrides module-naming- 244 | # style. 245 | #module-rgx= 246 | 247 | # Colon-delimited sets of names that determine each other's naming style when 248 | # the name regexes allow several styles. 249 | name-group= 250 | 251 | # Regular expression which should only match function or class names that do 252 | # not require a docstring. 253 | no-docstring-rgx=^_ 254 | 255 | # List of decorators that produce properties, such as abc.abstractproperty. Add 256 | # to this list to register other decorators that produce valid properties. 257 | # These decorators are taken in consideration only for invalid-name. 258 | property-classes=abc.abstractproperty 259 | 260 | # Naming style matching correct variable names. 261 | variable-naming-style=snake_case 262 | 263 | # Regular expression matching correct variable names. Overrides variable- 264 | # naming-style. 265 | #variable-rgx= 266 | 267 | 268 | [FORMAT] 269 | 270 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 271 | expected-line-ending-format= 272 | 273 | # Regexp for a line that is allowed to be longer than the limit. 274 | ignore-long-lines=^\s*(# )??$ 275 | 276 | # Number of spaces of indent required inside a hanging or continued line. 277 | indent-after-paren=4 278 | 279 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 280 | # tab). 281 | indent-string=' ' 282 | 283 | # Maximum number of characters on a single line. 284 | max-line-length=120 285 | 286 | # Maximum number of lines in a module. 287 | max-module-lines=1000 288 | 289 | # Allow the body of a class to be on the same line as the declaration if body 290 | # contains single statement. 291 | single-line-class-stmt=no 292 | 293 | # Allow the body of an if to be on the same line as the test if there is no 294 | # else. 295 | single-line-if-stmt=no 296 | 297 | 298 | [LOGGING] 299 | 300 | # The type of string formatting that logging methods do. `old` means using % 301 | # formatting, `new` is for `{}` formatting. 302 | logging-format-style=old 303 | 304 | # Logging modules to check that the string format arguments are in logging 305 | # function parameter format. 306 | logging-modules=logging 307 | 308 | 309 | [MISCELLANEOUS] 310 | 311 | # List of note tags to take in consideration, separated by a comma. 312 | notes=FIXME, 313 | XXX, 314 | TODO 315 | 316 | # Regular expression of note tags to take in consideration. 317 | #notes-rgx= 318 | 319 | 320 | [SIMILARITIES] 321 | 322 | # Comments are removed from the similarity computation 323 | ignore-comments=yes 324 | 325 | # Docstrings are removed from the similarity computation 326 | ignore-docstrings=yes 327 | 328 | # Imports are removed from the similarity computation 329 | ignore-imports=yes 330 | 331 | # Signatures are removed from the similarity computation 332 | ignore-signatures=no 333 | 334 | # Minimum lines number of a similarity. 335 | min-similarity-lines=4 336 | 337 | 338 | [SPELLING] 339 | 340 | # Limits count of emitted suggestions for spelling mistakes. 341 | max-spelling-suggestions=4 342 | 343 | # Spelling dictionary name. Available dictionaries: none. To make it work, 344 | # install the 'python-enchant' package. 345 | spelling-dict= 346 | 347 | # List of comma separated words that should be considered directives if they 348 | # appear and the beginning of a comment and should not be checked. 349 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 350 | 351 | # List of comma separated words that should not be checked. 352 | spelling-ignore-words= 353 | 354 | # A path to a file that contains the private dictionary; one word per line. 355 | spelling-private-dict-file= 356 | 357 | # Tells whether to store unknown words to the private dictionary (see the 358 | # --spelling-private-dict-file option) instead of raising a message. 359 | spelling-store-unknown-words=no 360 | 361 | 362 | [STRING] 363 | 364 | # This flag controls whether inconsistent-quotes generates a warning when the 365 | # character used as a quote delimiter is used inconsistently within a module. 366 | check-quote-consistency=no 367 | 368 | # This flag controls whether the implicit-str-concat should generate a warning 369 | # on implicit string concatenation in sequences defined over several lines. 370 | check-str-concat-over-line-jumps=no 371 | 372 | 373 | [TYPECHECK] 374 | 375 | # List of decorators that produce context managers, such as 376 | # contextlib.contextmanager. Add to this list to register other decorators that 377 | # produce valid context managers. 378 | contextmanager-decorators=contextlib.contextmanager 379 | 380 | # List of members which are set dynamically and missed by pylint inference 381 | # system, and so shouldn't trigger E1101 when accessed. Python regular 382 | # expressions are accepted. 383 | generated-members= 384 | 385 | # Tells whether missing members accessed in mixin class should be ignored. A 386 | # class is considered mixin if its name matches the mixin-class-rgx option. 387 | ignore-mixin-members=yes 388 | 389 | # Tells whether to warn about missing members when the owner of the attribute 390 | # is inferred to be None. 391 | ignore-none=yes 392 | 393 | # This flag controls whether pylint should warn about no-member and similar 394 | # checks whenever an opaque object is returned when inferring. The inference 395 | # can return multiple potential results while evaluating a Python object, but 396 | # some branches might not be evaluated, which results in partial inference. In 397 | # that case, it might be useful to still emit no-member and other checks for 398 | # the rest of the inferred objects. 399 | ignore-on-opaque-inference=yes 400 | 401 | # List of class names for which member attributes should not be checked (useful 402 | # for classes with dynamically set attributes). This supports the use of 403 | # qualified names. 404 | ignored-classes=optparse.Values,thread._local,_thread._local 405 | 406 | # List of module names for which member attributes should not be checked 407 | # (useful for modules/projects where namespaces are manipulated during runtime 408 | # and thus existing member attributes cannot be deduced by static analysis). It 409 | # supports qualified module names, as well as Unix pattern matching. 410 | ignored-modules= 411 | 412 | # Show a hint with possible names when a member name was not found. The aspect 413 | # of finding the hint is based on edit distance. 414 | missing-member-hint=yes 415 | 416 | # The minimum edit distance a name should have in order to be considered a 417 | # similar match for a missing member name. 418 | missing-member-hint-distance=1 419 | 420 | # The total number of similar names that should be taken in consideration when 421 | # showing a hint for a missing member. 422 | missing-member-max-choices=1 423 | 424 | # Regex pattern to define which classes are considered mixins ignore-mixin- 425 | # members is set to 'yes' 426 | mixin-class-rgx=.*[Mm]ixin 427 | 428 | # List of decorators that change the signature of a decorated function. 429 | signature-mutators= 430 | 431 | 432 | [VARIABLES] 433 | 434 | # List of additional names supposed to be defined in builtins. Remember that 435 | # you should avoid defining new builtins when possible. 436 | additional-builtins= 437 | 438 | # Tells whether unused global variables should be treated as a violation. 439 | allow-global-unused-variables=yes 440 | 441 | # List of names allowed to shadow builtins 442 | allowed-redefined-builtins= 443 | 444 | # List of strings which can identify a callback function by name. A callback 445 | # name must start or end with one of those strings. 446 | callbacks=cb_, 447 | _cb 448 | 449 | # A regular expression matching the name of dummy variables (i.e. expected to 450 | # not be used). 451 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 452 | 453 | # Argument names that match this expression will be ignored. Default to name 454 | # with leading underscore. 455 | ignored-argument-names=_.*|^ignored_|^unused_ 456 | 457 | # Tells whether we should check for unused import in __init__ files. 458 | init-import=no 459 | 460 | # List of qualified module names which can have objects that can redefine 461 | # builtins. 462 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 463 | 464 | 465 | [CLASSES] 466 | 467 | # Warn about protected attribute access inside special methods 468 | check-protected-access-in-special-methods=no 469 | 470 | # List of method names used to declare (i.e. assign) instance attributes. 471 | defining-attr-methods=__init__, 472 | __new__, 473 | setUp, 474 | __post_init__ 475 | 476 | # List of member names, which should be excluded from the protected access 477 | # warning. 478 | exclude-protected=_asdict, 479 | _fields, 480 | _replace, 481 | _source, 482 | _make 483 | 484 | # List of valid names for the first argument in a class method. 485 | valid-classmethod-first-arg=cls 486 | 487 | # List of valid names for the first argument in a metaclass class method. 488 | valid-metaclass-classmethod-first-arg=cls 489 | 490 | 491 | [DESIGN] 492 | 493 | # List of regular expressions of class ancestor names to ignore when counting 494 | # public methods (see R0903) 495 | exclude-too-few-public-methods= 496 | 497 | # List of qualified class names to ignore when counting class parents (see 498 | # R0901) 499 | ignored-parents= 500 | 501 | # Maximum number of arguments for function / method. 502 | max-args=5 503 | 504 | # Maximum number of attributes for a class (see R0902). 505 | max-attributes=7 506 | 507 | # Maximum number of boolean expressions in an if statement (see R0916). 508 | max-bool-expr=5 509 | 510 | # Maximum number of branch for function / method body. 511 | max-branches=12 512 | 513 | # Maximum number of locals for function / method body. 514 | max-locals=15 515 | 516 | # Maximum number of parents for a class (see R0901). 517 | max-parents=7 518 | 519 | # Maximum number of public methods for a class (see R0904). 520 | max-public-methods=20 521 | 522 | # Maximum number of return / yield for function / method body. 523 | max-returns=8 524 | 525 | # Maximum number of statements in function / method body. 526 | max-statements=50 527 | 528 | # Minimum number of public methods for a class (see R0903). 529 | min-public-methods=2 530 | 531 | 532 | [IMPORTS] 533 | 534 | # List of modules that can be imported at any level, not just the top level 535 | # one. 536 | allow-any-import-level= 537 | 538 | # Allow wildcard imports from modules that define __all__. 539 | allow-wildcard-with-all=no 540 | 541 | # Analyse import fallback blocks. This can be used to support both Python 2 and 542 | # 3 compatible code, which means that the block might have code that exists 543 | # only in one or another interpreter, leading to false positives when analysed. 544 | analyse-fallback-blocks=no 545 | 546 | # Deprecated modules which should not be used, separated by a comma. 547 | deprecated-modules= 548 | 549 | # Output a graph (.gv or any supported image format) of external dependencies 550 | # to the given file (report RP0402 must not be disabled). 551 | ext-import-graph= 552 | 553 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 554 | # external) dependencies to the given file (report RP0402 must not be 555 | # disabled). 556 | import-graph= 557 | 558 | # Output a graph (.gv or any supported image format) of internal dependencies 559 | # to the given file (report RP0402 must not be disabled). 560 | int-import-graph= 561 | 562 | # Force import order to recognize a module as part of the standard 563 | # compatibility libraries. 564 | known-standard-library= 565 | 566 | # Force import order to recognize a module as part of a third party library. 567 | known-third-party=enchant 568 | 569 | # Couples of modules and preferred modules, separated by a comma. 570 | preferred-modules= 571 | 572 | 573 | [EXCEPTIONS] 574 | 575 | # Exceptions that will emit a warning when being caught. Defaults to 576 | # "BaseException, Exception". 577 | overgeneral-exceptions=BaseException, 578 | Exception 579 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "anyio" 3 | version = "3.5.0" 4 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.6.2" 8 | 9 | [package.dependencies] 10 | idna = ">=2.8" 11 | sniffio = ">=1.1" 12 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 13 | 14 | [package.extras] 15 | doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 16 | test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] 17 | trio = ["trio (>=0.16)"] 18 | 19 | [[package]] 20 | name = "argcomplete" 21 | version = "1.12.3" 22 | description = "Bash tab completion for argparse" 23 | category = "dev" 24 | optional = false 25 | python-versions = "*" 26 | 27 | [package.dependencies] 28 | importlib-metadata = {version = ">=0.23,<5", markers = "python_version == \"3.7\""} 29 | 30 | [package.extras] 31 | test = ["coverage", "flake8", "pexpect", "wheel"] 32 | 33 | [[package]] 34 | name = "assertpy" 35 | version = "1.1" 36 | description = "Simple assertion library for unit testing in python with a fluent API" 37 | category = "dev" 38 | optional = false 39 | python-versions = "*" 40 | 41 | [[package]] 42 | name = "astroid" 43 | version = "2.11.4" 44 | description = "An abstract syntax tree for Python with inference support." 45 | category = "dev" 46 | optional = false 47 | python-versions = ">=3.6.2" 48 | 49 | [package.dependencies] 50 | lazy-object-proxy = ">=1.4.0" 51 | setuptools = ">=20.0" 52 | typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} 53 | typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} 54 | wrapt = ">=1.11,<2" 55 | 56 | [[package]] 57 | name = "asttokens" 58 | version = "2.0.5" 59 | description = "Annotate AST trees with source code positions" 60 | category = "dev" 61 | optional = false 62 | python-versions = "*" 63 | 64 | [package.dependencies] 65 | six = "*" 66 | 67 | [package.extras] 68 | test = ["astroid", "pytest"] 69 | 70 | [[package]] 71 | name = "atomicwrites" 72 | version = "1.4.0" 73 | description = "Atomic file writes." 74 | category = "dev" 75 | optional = false 76 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 77 | 78 | [[package]] 79 | name = "attrs" 80 | version = "21.4.0" 81 | description = "Classes Without Boilerplate" 82 | category = "dev" 83 | optional = false 84 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 85 | 86 | [package.extras] 87 | dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] 88 | docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] 89 | tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] 90 | tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] 91 | 92 | [[package]] 93 | name = "black" 94 | version = "22.3.0" 95 | description = "The uncompromising code formatter." 96 | category = "dev" 97 | optional = false 98 | python-versions = ">=3.6.2" 99 | 100 | [package.dependencies] 101 | click = ">=8.0.0" 102 | mypy-extensions = ">=0.4.3" 103 | pathspec = ">=0.9.0" 104 | platformdirs = ">=2" 105 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 106 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} 107 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 108 | 109 | [package.extras] 110 | colorama = ["colorama (>=0.4.3)"] 111 | d = ["aiohttp (>=3.7.4)"] 112 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 113 | uvloop = ["uvloop (>=0.15.2)"] 114 | 115 | [[package]] 116 | name = "certifi" 117 | version = "2021.10.8" 118 | description = "Python package for providing Mozilla's CA Bundle." 119 | category = "main" 120 | optional = false 121 | python-versions = "*" 122 | 123 | [[package]] 124 | name = "cfgv" 125 | version = "3.3.1" 126 | description = "Validate configuration and produce human readable error messages." 127 | category = "dev" 128 | optional = false 129 | python-versions = ">=3.6.1" 130 | 131 | [[package]] 132 | name = "charset-normalizer" 133 | version = "2.0.12" 134 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 135 | category = "main" 136 | optional = false 137 | python-versions = ">=3.5.0" 138 | 139 | [package.extras] 140 | unicode_backport = ["unicodedata2"] 141 | 142 | [[package]] 143 | name = "click" 144 | version = "8.1.3" 145 | description = "Composable command line interface toolkit" 146 | category = "dev" 147 | optional = false 148 | python-versions = ">=3.7" 149 | 150 | [package.dependencies] 151 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 152 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 153 | 154 | [[package]] 155 | name = "colorama" 156 | version = "0.4.4" 157 | description = "Cross-platform colored terminal text." 158 | category = "dev" 159 | optional = false 160 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 161 | 162 | [[package]] 163 | name = "commitizen" 164 | version = "2.24.0" 165 | description = "Python commitizen client tool" 166 | category = "dev" 167 | optional = false 168 | python-versions = ">=3.6.2,<4.0.0" 169 | 170 | [package.dependencies] 171 | argcomplete = ">=1.12.1,<2.0.0" 172 | colorama = ">=0.4.1,<0.5.0" 173 | decli = ">=0.5.2,<0.6.0" 174 | jinja2 = ">=2.10.3" 175 | packaging = ">=19,<22" 176 | pyyaml = ">=3.08" 177 | questionary = ">=1.4.0,<2.0.0" 178 | termcolor = ">=1.1,<2.0" 179 | tomlkit = ">=0.5.3,<1.0.0" 180 | typing-extensions = ">=4.0.1,<5.0.0" 181 | 182 | [[package]] 183 | name = "coverage" 184 | version = "6.3.2" 185 | description = "Code coverage measurement for Python" 186 | category = "dev" 187 | optional = false 188 | python-versions = ">=3.7" 189 | 190 | [package.dependencies] 191 | tomli = {version = "*", optional = true, markers = "extra == \"toml\""} 192 | 193 | [package.extras] 194 | toml = ["tomli"] 195 | 196 | [[package]] 197 | name = "decli" 198 | version = "0.5.2" 199 | description = "Minimal, easy-to-use, declarative cli tool" 200 | category = "dev" 201 | optional = false 202 | python-versions = ">=3.6" 203 | 204 | [[package]] 205 | name = "deprecated" 206 | version = "1.2.13" 207 | description = "Python @deprecated decorator to deprecate old python classes, functions or methods." 208 | category = "dev" 209 | optional = false 210 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 211 | 212 | [package.dependencies] 213 | wrapt = ">=1.10,<2" 214 | 215 | [package.extras] 216 | dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] 217 | 218 | [[package]] 219 | name = "devtools" 220 | version = "0.8.0" 221 | description = "Python's missing debug print command and other development tools." 222 | category = "dev" 223 | optional = false 224 | python-versions = ">=3.6" 225 | 226 | [package.dependencies] 227 | asttokens = ">=2.0.0,<3.0.0" 228 | executing = ">=0.8.0,<1.0.0" 229 | 230 | [package.extras] 231 | pygments = ["Pygments (>=2.2.0)"] 232 | 233 | [[package]] 234 | name = "dill" 235 | version = "0.3.4" 236 | description = "serialize all of python" 237 | category = "dev" 238 | optional = false 239 | python-versions = ">=2.7, !=3.0.*" 240 | 241 | [package.extras] 242 | graph = ["objgraph (>=1.7.2)"] 243 | 244 | [[package]] 245 | name = "distlib" 246 | version = "0.3.4" 247 | description = "Distribution utilities" 248 | category = "dev" 249 | optional = false 250 | python-versions = "*" 251 | 252 | [[package]] 253 | name = "docopt" 254 | version = "0.6.2" 255 | description = "Pythonic argument parser, that will make you smile" 256 | category = "dev" 257 | optional = false 258 | python-versions = "*" 259 | 260 | [[package]] 261 | name = "executing" 262 | version = "0.8.3" 263 | description = "Get the currently executing AST node of a frame, and other information" 264 | category = "dev" 265 | optional = false 266 | python-versions = "*" 267 | 268 | [[package]] 269 | name = "fakeredis" 270 | version = "1.7.1" 271 | description = "Fake implementation of redis API for testing purposes." 272 | category = "dev" 273 | optional = false 274 | python-versions = ">=3.5" 275 | 276 | [package.dependencies] 277 | packaging = "*" 278 | redis = "<4.2.0" 279 | six = ">=1.12" 280 | sortedcontainers = "*" 281 | 282 | [package.extras] 283 | aioredis = ["aioredis"] 284 | lua = ["lupa"] 285 | 286 | [[package]] 287 | name = "fastapi" 288 | version = "0.72.0" 289 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 290 | category = "dev" 291 | optional = false 292 | python-versions = ">=3.6.1" 293 | 294 | [package.dependencies] 295 | pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" 296 | starlette = "0.17.1" 297 | 298 | [package.extras] 299 | all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,<5.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] 300 | dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] 301 | doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "typer-cli (>=0.0.12,<0.0.13)"] 302 | test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==21.9b0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.1.7)", "types-orjson (==3.6.0)", "types-ujson (==0.1.1)", "ujson (>=4.0.1,<5.0.0)"] 303 | 304 | [[package]] 305 | name = "filelock" 306 | version = "3.6.0" 307 | description = "A platform independent file lock." 308 | category = "dev" 309 | optional = false 310 | python-versions = ">=3.7" 311 | 312 | [package.extras] 313 | docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] 314 | testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] 315 | 316 | [[package]] 317 | name = "glob2" 318 | version = "0.7" 319 | description = "Version of the glob module that can capture patterns and supports recursive wildcards" 320 | category = "dev" 321 | optional = false 322 | python-versions = "*" 323 | 324 | [[package]] 325 | name = "identify" 326 | version = "2.5.0" 327 | description = "File identification library for Python" 328 | category = "dev" 329 | optional = false 330 | python-versions = ">=3.7" 331 | 332 | [package.extras] 333 | license = ["ukkonen"] 334 | 335 | [[package]] 336 | name = "idna" 337 | version = "3.3" 338 | description = "Internationalized Domain Names in Applications (IDNA)" 339 | category = "main" 340 | optional = false 341 | python-versions = ">=3.5" 342 | 343 | [[package]] 344 | name = "importlib-metadata" 345 | version = "4.11.3" 346 | description = "Read metadata from Python packages" 347 | category = "dev" 348 | optional = false 349 | python-versions = ">=3.7" 350 | 351 | [package.dependencies] 352 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 353 | zipp = ">=0.5" 354 | 355 | [package.extras] 356 | docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] 357 | perf = ["ipython"] 358 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] 359 | 360 | [[package]] 361 | name = "iniconfig" 362 | version = "1.1.1" 363 | description = "iniconfig: brain-dead simple config-ini parsing" 364 | category = "dev" 365 | optional = false 366 | python-versions = "*" 367 | 368 | [[package]] 369 | name = "isort" 370 | version = "5.10.1" 371 | description = "A Python utility / library to sort Python imports." 372 | category = "dev" 373 | optional = false 374 | python-versions = ">=3.6.1,<4.0" 375 | 376 | [package.extras] 377 | colors = ["colorama (>=0.4.3,<0.5.0)"] 378 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 379 | plugins = ["setuptools"] 380 | requirements_deprecated_finder = ["pip-api", "pipreqs"] 381 | 382 | [[package]] 383 | name = "jinja2" 384 | version = "3.1.2" 385 | description = "A very fast and expressive template engine." 386 | category = "dev" 387 | optional = false 388 | python-versions = ">=3.7" 389 | 390 | [package.dependencies] 391 | MarkupSafe = ">=2.0" 392 | 393 | [package.extras] 394 | i18n = ["Babel (>=2.7)"] 395 | 396 | [[package]] 397 | name = "lazy-object-proxy" 398 | version = "1.7.1" 399 | description = "A fast and thorough lazy object proxy." 400 | category = "dev" 401 | optional = false 402 | python-versions = ">=3.6" 403 | 404 | [[package]] 405 | name = "Mako" 406 | version = "1.2.2" 407 | description = "A super-fast templating language that borrows the best ideas from the existing templating languages." 408 | category = "dev" 409 | optional = false 410 | python-versions = ">=3.7" 411 | 412 | [package.dependencies] 413 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 414 | MarkupSafe = ">=0.9.2" 415 | 416 | [package.extras] 417 | babel = ["Babel"] 418 | lingua = ["lingua"] 419 | testing = ["pytest"] 420 | 421 | [[package]] 422 | name = "markupsafe" 423 | version = "2.1.1" 424 | description = "Safely add untrusted strings to HTML/XML markup." 425 | category = "dev" 426 | optional = false 427 | python-versions = ">=3.7" 428 | 429 | [[package]] 430 | name = "mccabe" 431 | version = "0.7.0" 432 | description = "McCabe checker, plugin for flake8" 433 | category = "dev" 434 | optional = false 435 | python-versions = ">=3.6" 436 | 437 | [[package]] 438 | name = "mypy" 439 | version = "0.931" 440 | description = "Optional static typing for Python" 441 | category = "dev" 442 | optional = false 443 | python-versions = ">=3.6" 444 | 445 | [package.dependencies] 446 | mypy-extensions = ">=0.4.3" 447 | tomli = ">=1.1.0" 448 | typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} 449 | typing-extensions = ">=3.10" 450 | 451 | [package.extras] 452 | dmypy = ["psutil (>=4.0)"] 453 | python2 = ["typed-ast (>=1.4.0,<2)"] 454 | 455 | [[package]] 456 | name = "mypy-extensions" 457 | version = "0.4.3" 458 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 459 | category = "dev" 460 | optional = false 461 | python-versions = "*" 462 | 463 | [[package]] 464 | name = "nodeenv" 465 | version = "1.6.0" 466 | description = "Node.js virtual environment builder" 467 | category = "dev" 468 | optional = false 469 | python-versions = "*" 470 | 471 | [[package]] 472 | name = "packaging" 473 | version = "21.3" 474 | description = "Core utilities for Python packages" 475 | category = "dev" 476 | optional = false 477 | python-versions = ">=3.6" 478 | 479 | [package.dependencies] 480 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 481 | 482 | [[package]] 483 | name = "parse" 484 | version = "1.19.0" 485 | description = "parse() is the opposite of format()" 486 | category = "dev" 487 | optional = false 488 | python-versions = "*" 489 | 490 | [[package]] 491 | name = "parse-type" 492 | version = "0.6.0" 493 | description = "Simplifies to build parse types based on the parse module" 494 | category = "dev" 495 | optional = false 496 | python-versions = ">=2.7, !=3.0.*, !=3.1.*" 497 | 498 | [package.dependencies] 499 | parse = {version = ">=1.18.0", markers = "python_version >= \"3.0\""} 500 | six = ">=1.11" 501 | 502 | [package.extras] 503 | develop = ["coverage (>=4.4)", "pytest (<5.0)", "pytest (>=5.0)", "pytest-cov", "pytest-html (>=1.19.0)", "tox (>=2.8)"] 504 | docs = ["sphinx (>=1.2)"] 505 | 506 | [[package]] 507 | name = "pathspec" 508 | version = "0.9.0" 509 | description = "Utility library for gitignore style pattern matching of file paths." 510 | category = "dev" 511 | optional = false 512 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 513 | 514 | [[package]] 515 | name = "platformdirs" 516 | version = "2.5.2" 517 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 518 | category = "dev" 519 | optional = false 520 | python-versions = ">=3.7" 521 | 522 | [package.extras] 523 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] 524 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 525 | 526 | [[package]] 527 | name = "pluggy" 528 | version = "1.0.0" 529 | description = "plugin and hook calling mechanisms for python" 530 | category = "dev" 531 | optional = false 532 | python-versions = ">=3.6" 533 | 534 | [package.dependencies] 535 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 536 | 537 | [package.extras] 538 | dev = ["pre-commit", "tox"] 539 | testing = ["pytest", "pytest-benchmark"] 540 | 541 | [[package]] 542 | name = "pre-commit" 543 | version = "2.18.1" 544 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 545 | category = "dev" 546 | optional = false 547 | python-versions = ">=3.7" 548 | 549 | [package.dependencies] 550 | cfgv = ">=2.0.0" 551 | identify = ">=1.0.0" 552 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 553 | nodeenv = ">=0.11.1" 554 | pyyaml = ">=5.1" 555 | toml = "*" 556 | virtualenv = ">=20.0.8" 557 | 558 | [[package]] 559 | name = "prompt-toolkit" 560 | version = "3.0.29" 561 | description = "Library for building powerful interactive command lines in Python" 562 | category = "dev" 563 | optional = false 564 | python-versions = ">=3.6.2" 565 | 566 | [package.dependencies] 567 | wcwidth = "*" 568 | 569 | [[package]] 570 | name = "py" 571 | version = "1.11.0" 572 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 573 | category = "dev" 574 | optional = false 575 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 576 | 577 | [[package]] 578 | name = "pydantic" 579 | version = "1.9.0" 580 | description = "Data validation and settings management using python 3.6 type hinting" 581 | category = "dev" 582 | optional = false 583 | python-versions = ">=3.6.1" 584 | 585 | [package.dependencies] 586 | typing-extensions = ">=3.7.4.3" 587 | 588 | [package.extras] 589 | dotenv = ["python-dotenv (>=0.10.4)"] 590 | email = ["email-validator (>=1.0.3)"] 591 | 592 | [[package]] 593 | name = "pylint" 594 | version = "2.13.8" 595 | description = "python code static checker" 596 | category = "dev" 597 | optional = false 598 | python-versions = ">=3.6.2" 599 | 600 | [package.dependencies] 601 | astroid = ">=2.11.3,<=2.12.0-dev0" 602 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 603 | dill = ">=0.2" 604 | isort = ">=4.2.5,<6" 605 | mccabe = ">=0.6,<0.8" 606 | platformdirs = ">=2.2.0" 607 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 608 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 609 | 610 | [package.extras] 611 | testutil = ["gitpython (>3)"] 612 | 613 | [[package]] 614 | name = "pylint-pytest" 615 | version = "1.1.2" 616 | description = "A Pylint plugin to suppress pytest-related false positives." 617 | category = "dev" 618 | optional = false 619 | python-versions = ">=3.6" 620 | 621 | [package.dependencies] 622 | pylint = "*" 623 | pytest = ">=4.6" 624 | 625 | [[package]] 626 | name = "pyparsing" 627 | version = "3.0.8" 628 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 629 | category = "dev" 630 | optional = false 631 | python-versions = ">=3.6.8" 632 | 633 | [package.extras] 634 | diagrams = ["jinja2", "railroad-diagrams"] 635 | 636 | [[package]] 637 | name = "pytest" 638 | version = "6.2.5" 639 | description = "pytest: simple powerful testing with Python" 640 | category = "dev" 641 | optional = false 642 | python-versions = ">=3.6" 643 | 644 | [package.dependencies] 645 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 646 | attrs = ">=19.2.0" 647 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 648 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 649 | iniconfig = "*" 650 | packaging = "*" 651 | pluggy = ">=0.12,<2.0" 652 | py = ">=1.8.2" 653 | toml = "*" 654 | 655 | [package.extras] 656 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 657 | 658 | [[package]] 659 | name = "pytest-asyncio" 660 | version = "0.17.2" 661 | description = "Pytest support for asyncio" 662 | category = "dev" 663 | optional = false 664 | python-versions = ">=3.7" 665 | 666 | [package.dependencies] 667 | pytest = ">=6.1.0" 668 | typing-extensions = {version = ">=4.0", markers = "python_version < \"3.8\""} 669 | 670 | [package.extras] 671 | testing = ["coverage (==6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (==0.931)"] 672 | 673 | [[package]] 674 | name = "pytest-bdd" 675 | version = "5.0.0" 676 | description = "BDD for pytest" 677 | category = "dev" 678 | optional = false 679 | python-versions = ">=3.6" 680 | 681 | [package.dependencies] 682 | glob2 = "*" 683 | Mako = "*" 684 | parse = "*" 685 | parse-type = "*" 686 | py = "*" 687 | pytest = ">=4.3" 688 | 689 | [[package]] 690 | name = "pytest-cov" 691 | version = "3.0.0" 692 | description = "Pytest plugin for measuring coverage." 693 | category = "dev" 694 | optional = false 695 | python-versions = ">=3.6" 696 | 697 | [package.dependencies] 698 | coverage = {version = ">=5.2.1", extras = ["toml"]} 699 | pytest = ">=4.6" 700 | 701 | [package.extras] 702 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 703 | 704 | [[package]] 705 | name = "pytest-pylint" 706 | version = "0.18.0" 707 | description = "pytest plugin to check source code with pylint" 708 | category = "dev" 709 | optional = false 710 | python-versions = ">=3.5" 711 | 712 | [package.dependencies] 713 | pylint = ">=2.3.0" 714 | pytest = ">=5.4" 715 | toml = ">=0.7.1" 716 | 717 | [[package]] 718 | name = "pytest-randomly" 719 | version = "3.11.0" 720 | description = "Pytest plugin to randomly order tests and control random.seed." 721 | category = "dev" 722 | optional = false 723 | python-versions = ">=3.7" 724 | 725 | [package.dependencies] 726 | importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} 727 | pytest = "*" 728 | 729 | [[package]] 730 | name = "pytest-watch" 731 | version = "4.2.0" 732 | description = "Local continuous test runner with pytest and watchdog." 733 | category = "dev" 734 | optional = false 735 | python-versions = "*" 736 | 737 | [package.dependencies] 738 | colorama = ">=0.3.3" 739 | docopt = ">=0.4.0" 740 | pytest = ">=2.6.4" 741 | watchdog = ">=0.6.0" 742 | 743 | [[package]] 744 | name = "pyyaml" 745 | version = "6.0" 746 | description = "YAML parser and emitter for Python" 747 | category = "dev" 748 | optional = false 749 | python-versions = ">=3.6" 750 | 751 | [[package]] 752 | name = "questionary" 753 | version = "1.10.0" 754 | description = "Python library to build pretty command line user prompts ⭐️" 755 | category = "dev" 756 | optional = false 757 | python-versions = ">=3.6,<4.0" 758 | 759 | [package.dependencies] 760 | prompt_toolkit = ">=2.0,<4.0" 761 | 762 | [package.extras] 763 | docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)"] 764 | 765 | [[package]] 766 | name = "redis" 767 | version = "4.1.4" 768 | description = "Python client for Redis database and key-value store" 769 | category = "dev" 770 | optional = false 771 | python-versions = ">=3.6" 772 | 773 | [package.dependencies] 774 | deprecated = ">=1.2.3" 775 | importlib-metadata = {version = ">=1.0", markers = "python_version < \"3.8\""} 776 | packaging = ">=20.4" 777 | 778 | [package.extras] 779 | hiredis = ["hiredis (>=1.0.0)"] 780 | ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] 781 | 782 | [[package]] 783 | name = "requests" 784 | version = "2.27.1" 785 | description = "Python HTTP for Humans." 786 | category = "main" 787 | optional = false 788 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 789 | 790 | [package.dependencies] 791 | certifi = ">=2017.4.17" 792 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 793 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 794 | urllib3 = ">=1.21.1,<1.27" 795 | 796 | [package.extras] 797 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 798 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 799 | 800 | [[package]] 801 | name = "setuptools" 802 | version = "65.3.0" 803 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 804 | category = "dev" 805 | optional = false 806 | python-versions = ">=3.7" 807 | 808 | [package.extras] 809 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 810 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 811 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 812 | 813 | [[package]] 814 | name = "six" 815 | version = "1.16.0" 816 | description = "Python 2 and 3 compatibility utilities" 817 | category = "dev" 818 | optional = false 819 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 820 | 821 | [[package]] 822 | name = "sniffio" 823 | version = "1.2.0" 824 | description = "Sniff out which async library your code is running under" 825 | category = "dev" 826 | optional = false 827 | python-versions = ">=3.5" 828 | 829 | [[package]] 830 | name = "sortedcontainers" 831 | version = "2.4.0" 832 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 833 | category = "dev" 834 | optional = false 835 | python-versions = "*" 836 | 837 | [[package]] 838 | name = "starlette" 839 | version = "0.17.1" 840 | description = "The little ASGI library that shines." 841 | category = "dev" 842 | optional = false 843 | python-versions = ">=3.6" 844 | 845 | [package.dependencies] 846 | anyio = ">=3.0.0,<4" 847 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 848 | 849 | [package.extras] 850 | full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] 851 | 852 | [[package]] 853 | name = "termcolor" 854 | version = "1.1.0" 855 | description = "ANSII Color formatting for output in terminal." 856 | category = "dev" 857 | optional = false 858 | python-versions = "*" 859 | 860 | [[package]] 861 | name = "toml" 862 | version = "0.10.2" 863 | description = "Python Library for Tom's Obvious, Minimal Language" 864 | category = "dev" 865 | optional = false 866 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 867 | 868 | [[package]] 869 | name = "tomli" 870 | version = "2.0.1" 871 | description = "A lil' TOML parser" 872 | category = "dev" 873 | optional = false 874 | python-versions = ">=3.7" 875 | 876 | [[package]] 877 | name = "tomlkit" 878 | version = "0.10.2" 879 | description = "Style preserving TOML library" 880 | category = "dev" 881 | optional = false 882 | python-versions = ">=3.6,<4.0" 883 | 884 | [[package]] 885 | name = "tox" 886 | version = "3.25.0" 887 | description = "tox is a generic virtualenv management and test command line tool" 888 | category = "dev" 889 | optional = false 890 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 891 | 892 | [package.dependencies] 893 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 894 | filelock = ">=3.0.0" 895 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 896 | packaging = ">=14" 897 | pluggy = ">=0.12.0" 898 | py = ">=1.4.17" 899 | six = ">=1.14.0" 900 | toml = ">=0.9.4" 901 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 902 | 903 | [package.extras] 904 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 905 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] 906 | 907 | [[package]] 908 | name = "typed-ast" 909 | version = "1.5.3" 910 | description = "a fork of Python 2 and 3 ast modules with type comment support" 911 | category = "dev" 912 | optional = false 913 | python-versions = ">=3.6" 914 | 915 | [[package]] 916 | name = "typing-extensions" 917 | version = "4.2.0" 918 | description = "Backported and Experimental Type Hints for Python 3.7+" 919 | category = "dev" 920 | optional = false 921 | python-versions = ">=3.7" 922 | 923 | [[package]] 924 | name = "urllib3" 925 | version = "1.26.9" 926 | description = "HTTP library with thread-safe connection pooling, file post, and more." 927 | category = "main" 928 | optional = false 929 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 930 | 931 | [package.extras] 932 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 933 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] 934 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 935 | 936 | [[package]] 937 | name = "virtualenv" 938 | version = "20.14.1" 939 | description = "Virtual Python Environment builder" 940 | category = "dev" 941 | optional = false 942 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 943 | 944 | [package.dependencies] 945 | distlib = ">=0.3.1,<1" 946 | filelock = ">=3.2,<4" 947 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 948 | platformdirs = ">=2,<3" 949 | six = ">=1.9.0,<2" 950 | 951 | [package.extras] 952 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] 953 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] 954 | 955 | [[package]] 956 | name = "watchdog" 957 | version = "2.1.7" 958 | description = "Filesystem events monitoring" 959 | category = "dev" 960 | optional = false 961 | python-versions = ">=3.6" 962 | 963 | [package.extras] 964 | watchmedo = ["PyYAML (>=3.10)"] 965 | 966 | [[package]] 967 | name = "wcwidth" 968 | version = "0.2.5" 969 | description = "Measures the displayed width of unicode strings in a terminal" 970 | category = "dev" 971 | optional = false 972 | python-versions = "*" 973 | 974 | [[package]] 975 | name = "wrapt" 976 | version = "1.14.1" 977 | description = "Module for decorators, wrappers and monkey patching." 978 | category = "dev" 979 | optional = false 980 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 981 | 982 | [[package]] 983 | name = "zipp" 984 | version = "3.8.0" 985 | description = "Backport of pathlib-compatible object wrapper for zip files" 986 | category = "dev" 987 | optional = false 988 | python-versions = ">=3.7" 989 | 990 | [package.extras] 991 | docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] 992 | testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 993 | 994 | [extras] 995 | fastapi = [] 996 | redis = [] 997 | 998 | [metadata] 999 | lock-version = "1.1" 1000 | python-versions = "^3.7" 1001 | content-hash = "25c1e86228f24db2eb0734e2ca0cba5de807a447d2d347cf99fc80f4198e4028" 1002 | 1003 | [metadata.files] 1004 | anyio = [ 1005 | {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, 1006 | {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, 1007 | ] 1008 | argcomplete = [ 1009 | {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"}, 1010 | {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, 1011 | ] 1012 | assertpy = [ 1013 | {file = "assertpy-1.1.tar.gz", hash = "sha256:acc64329934ad71a3221de185517a43af33e373bb44dc05b5a9b174394ef4833"}, 1014 | ] 1015 | astroid = [ 1016 | {file = "astroid-2.11.4-py3-none-any.whl", hash = "sha256:da0632b7c046d8361dfe1b1abb2e085a38624961fabe2997565a9c06c1be9d9a"}, 1017 | {file = "astroid-2.11.4.tar.gz", hash = "sha256:561dc6015eecce7e696ff7e3b40434bc56831afeff783f0ea853e19c4f635c06"}, 1018 | ] 1019 | asttokens = [ 1020 | {file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"}, 1021 | {file = "asttokens-2.0.5.tar.gz", hash = "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5"}, 1022 | ] 1023 | atomicwrites = [ 1024 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 1025 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 1026 | ] 1027 | attrs = [ 1028 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 1029 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 1030 | ] 1031 | black = [ 1032 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, 1033 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, 1034 | {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, 1035 | {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, 1036 | {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, 1037 | {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, 1038 | {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, 1039 | {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, 1040 | {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, 1041 | {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, 1042 | {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, 1043 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, 1044 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, 1045 | {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, 1046 | {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, 1047 | {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, 1048 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, 1049 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, 1050 | {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, 1051 | {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, 1052 | {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, 1053 | {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, 1054 | {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, 1055 | ] 1056 | certifi = [ 1057 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 1058 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 1059 | ] 1060 | cfgv = [ 1061 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 1062 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 1063 | ] 1064 | charset-normalizer = [ 1065 | {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, 1066 | {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, 1067 | ] 1068 | click = [ 1069 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 1070 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 1071 | ] 1072 | colorama = [ 1073 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 1074 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 1075 | ] 1076 | commitizen = [ 1077 | {file = "commitizen-2.24.0-py3-none-any.whl", hash = "sha256:08901b176eac6a224761d613b58fb8b19bc7d00a49282a4d4bc39e3bdb3afb50"}, 1078 | {file = "commitizen-2.24.0.tar.gz", hash = "sha256:c867c26a394b255a93a8a225dae793dd361b25160be39015d2aa75d730728295"}, 1079 | ] 1080 | coverage = [ 1081 | {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, 1082 | {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, 1083 | {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, 1084 | {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, 1085 | {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, 1086 | {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, 1087 | {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, 1088 | {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, 1089 | {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, 1090 | {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, 1091 | {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, 1092 | {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, 1093 | {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, 1094 | {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, 1095 | {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, 1096 | {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, 1097 | {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, 1098 | {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, 1099 | {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, 1100 | {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, 1101 | {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, 1102 | {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, 1103 | {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, 1104 | {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, 1105 | {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, 1106 | {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, 1107 | {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, 1108 | {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, 1109 | {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, 1110 | {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, 1111 | {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, 1112 | {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, 1113 | {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, 1114 | {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, 1115 | {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, 1116 | {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, 1117 | {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, 1118 | {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, 1119 | {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, 1120 | {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, 1121 | {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, 1122 | ] 1123 | decli = [ 1124 | {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, 1125 | {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, 1126 | ] 1127 | deprecated = [ 1128 | {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, 1129 | {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, 1130 | ] 1131 | devtools = [ 1132 | {file = "devtools-0.8.0-py3-none-any.whl", hash = "sha256:00717ef184223cf36c65bbd17c6eb412f8a7564f47957f9e8b2b7610661b17fb"}, 1133 | {file = "devtools-0.8.0.tar.gz", hash = "sha256:6162a2f61c70242479dff3163e7837e6a9bf32451661af1347bfa3115602af16"}, 1134 | ] 1135 | dill = [ 1136 | {file = "dill-0.3.4-py2.py3-none-any.whl", hash = "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f"}, 1137 | {file = "dill-0.3.4.zip", hash = "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675"}, 1138 | ] 1139 | distlib = [ 1140 | {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, 1141 | {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, 1142 | ] 1143 | docopt = [ 1144 | {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, 1145 | ] 1146 | executing = [ 1147 | {file = "executing-0.8.3-py2.py3-none-any.whl", hash = "sha256:d1eef132db1b83649a3905ca6dd8897f71ac6f8cac79a7e58a1a09cf137546c9"}, 1148 | {file = "executing-0.8.3.tar.gz", hash = "sha256:c6554e21c6b060590a6d3be4b82fb78f8f0194d809de5ea7df1c093763311501"}, 1149 | ] 1150 | fakeredis = [ 1151 | {file = "fakeredis-1.7.1-py3-none-any.whl", hash = "sha256:be3668e50f6b57d5fc4abfd27f9f655bed07a2c5aecfc8b15d0aad59f997c1ba"}, 1152 | {file = "fakeredis-1.7.1.tar.gz", hash = "sha256:7c2c4ba1b42e0a75337c54b777bf0671056b4569650e3ff927e4b9b385afc8ec"}, 1153 | ] 1154 | fastapi = [ 1155 | {file = "fastapi-0.72.0-py3-none-any.whl", hash = "sha256:7421a2f30e9ed1866874cff089733d4f9a0cd4f49b6ea3995c0de75e32bbb52f"}, 1156 | {file = "fastapi-0.72.0.tar.gz", hash = "sha256:019ec52c00581bc055e6dfb621aaa9c2a56007c283839305412e1073a777eaf1"}, 1157 | ] 1158 | filelock = [ 1159 | {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, 1160 | {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, 1161 | ] 1162 | glob2 = [ 1163 | {file = "glob2-0.7.tar.gz", hash = "sha256:85c3dbd07c8aa26d63d7aacee34fa86e9a91a3873bc30bf62ec46e531f92ab8c"}, 1164 | ] 1165 | identify = [ 1166 | {file = "identify-2.5.0-py2.py3-none-any.whl", hash = "sha256:3acfe15a96e4272b4ec5662ee3e231ceba976ef63fd9980ed2ce9cc415df393f"}, 1167 | {file = "identify-2.5.0.tar.gz", hash = "sha256:c83af514ea50bf2be2c4a3f2fb349442b59dc87284558ae9ff54191bff3541d2"}, 1168 | ] 1169 | idna = [ 1170 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 1171 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 1172 | ] 1173 | importlib-metadata = [ 1174 | {file = "importlib_metadata-4.11.3-py3-none-any.whl", hash = "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6"}, 1175 | {file = "importlib_metadata-4.11.3.tar.gz", hash = "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"}, 1176 | ] 1177 | iniconfig = [ 1178 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 1179 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 1180 | ] 1181 | isort = [ 1182 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 1183 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 1184 | ] 1185 | jinja2 = [ 1186 | {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, 1187 | {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, 1188 | ] 1189 | lazy-object-proxy = [ 1190 | {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, 1191 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, 1192 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, 1193 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, 1194 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, 1195 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, 1196 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, 1197 | {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, 1198 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, 1199 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, 1200 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, 1201 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, 1202 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, 1203 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, 1204 | {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, 1205 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, 1206 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, 1207 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, 1208 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, 1209 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, 1210 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, 1211 | {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, 1212 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, 1213 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, 1214 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, 1215 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, 1216 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, 1217 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, 1218 | {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, 1219 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, 1220 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, 1221 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, 1222 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, 1223 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, 1224 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, 1225 | {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, 1226 | {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, 1227 | ] 1228 | Mako = [ 1229 | {file = "Mako-1.2.2-py3-none-any.whl", hash = "sha256:8efcb8004681b5f71d09c983ad5a9e6f5c40601a6ec469148753292abc0da534"}, 1230 | {file = "Mako-1.2.2.tar.gz", hash = "sha256:3724869b363ba630a272a5f89f68c070352137b8fd1757650017b7e06fda163f"}, 1231 | ] 1232 | markupsafe = [ 1233 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, 1234 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, 1235 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, 1236 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, 1237 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, 1238 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, 1239 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, 1240 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, 1241 | {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, 1242 | {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, 1243 | {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, 1244 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, 1245 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, 1246 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, 1247 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, 1248 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, 1249 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, 1250 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, 1251 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, 1252 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, 1253 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, 1254 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, 1255 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, 1256 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, 1257 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, 1258 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, 1259 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, 1260 | {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, 1261 | {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, 1262 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, 1263 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, 1264 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, 1265 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, 1266 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, 1267 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, 1268 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, 1269 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, 1270 | {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, 1271 | {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, 1272 | {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, 1273 | ] 1274 | mccabe = [ 1275 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 1276 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 1277 | ] 1278 | mypy = [ 1279 | {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"}, 1280 | {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"}, 1281 | {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"}, 1282 | {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"}, 1283 | {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"}, 1284 | {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"}, 1285 | {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"}, 1286 | {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"}, 1287 | {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"}, 1288 | {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"}, 1289 | {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"}, 1290 | {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"}, 1291 | {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"}, 1292 | {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"}, 1293 | {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"}, 1294 | {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"}, 1295 | {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"}, 1296 | {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"}, 1297 | {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"}, 1298 | {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"}, 1299 | ] 1300 | mypy-extensions = [ 1301 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 1302 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 1303 | ] 1304 | nodeenv = [ 1305 | {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 1306 | {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 1307 | ] 1308 | packaging = [ 1309 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 1310 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 1311 | ] 1312 | parse = [ 1313 | {file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"}, 1314 | ] 1315 | parse-type = [ 1316 | {file = "parse_type-0.6.0-py2.py3-none-any.whl", hash = "sha256:c148e88436bd54dab16484108e882be3367f44952c649c9cd6b82a7370b650cb"}, 1317 | {file = "parse_type-0.6.0.tar.gz", hash = "sha256:20b43c660e48ed47f433bce5873a2a3d4b9b6a7ba47bd7f7d2a7cec4bec5551f"}, 1318 | ] 1319 | pathspec = [ 1320 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 1321 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 1322 | ] 1323 | platformdirs = [ 1324 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 1325 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 1326 | ] 1327 | pluggy = [ 1328 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 1329 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 1330 | ] 1331 | pre-commit = [ 1332 | {file = "pre_commit-2.18.1-py2.py3-none-any.whl", hash = "sha256:02226e69564ebca1a070bd1f046af866aa1c318dbc430027c50ab832ed2b73f2"}, 1333 | {file = "pre_commit-2.18.1.tar.gz", hash = "sha256:5d445ee1fa8738d506881c5d84f83c62bb5be6b2838e32207433647e8e5ebe10"}, 1334 | ] 1335 | prompt-toolkit = [ 1336 | {file = "prompt_toolkit-3.0.29-py3-none-any.whl", hash = "sha256:62291dad495e665fca0bda814e342c69952086afb0f4094d0893d357e5c78752"}, 1337 | {file = "prompt_toolkit-3.0.29.tar.gz", hash = "sha256:bd640f60e8cecd74f0dc249713d433ace2ddc62b65ee07f96d358e0b152b6ea7"}, 1338 | ] 1339 | py = [ 1340 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 1341 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 1342 | ] 1343 | pydantic = [ 1344 | {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, 1345 | {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, 1346 | {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, 1347 | {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, 1348 | {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, 1349 | {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, 1350 | {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, 1351 | {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, 1352 | {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, 1353 | {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, 1354 | {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, 1355 | {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, 1356 | {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, 1357 | {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, 1358 | {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, 1359 | {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, 1360 | {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, 1361 | {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, 1362 | {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, 1363 | {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, 1364 | {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, 1365 | {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, 1366 | {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, 1367 | {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, 1368 | {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, 1369 | {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, 1370 | {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, 1371 | {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, 1372 | {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, 1373 | {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, 1374 | {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, 1375 | {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, 1376 | {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, 1377 | {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, 1378 | {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, 1379 | ] 1380 | pylint = [ 1381 | {file = "pylint-2.13.8-py3-none-any.whl", hash = "sha256:f87e863a0b08f64b5230e7e779bcb75276346995737b2c0dc2793070487b1ff6"}, 1382 | {file = "pylint-2.13.8.tar.gz", hash = "sha256:ced8968c3b699df0615e2a709554dec3ddac2f5cd06efadb69554a69eeca364a"}, 1383 | ] 1384 | pylint-pytest = [ 1385 | {file = "pylint_pytest-1.1.2-py2.py3-none-any.whl", hash = "sha256:fb20ef318081cee3d5febc631a7b9c40fa356b05e4f769d6e60a337e58c8879b"}, 1386 | ] 1387 | pyparsing = [ 1388 | {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, 1389 | {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, 1390 | ] 1391 | pytest = [ 1392 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 1393 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 1394 | ] 1395 | pytest-asyncio = [ 1396 | {file = "pytest-asyncio-0.17.2.tar.gz", hash = "sha256:6d895b02432c028e6957d25fc936494e78c6305736e785d9fee408b1efbc7ff4"}, 1397 | {file = "pytest_asyncio-0.17.2-py3-none-any.whl", hash = "sha256:e0fe5dbea40516b661ef1bcfe0bd9461c2847c4ef4bb40012324f2454fb7d56d"}, 1398 | ] 1399 | pytest-bdd = [ 1400 | {file = "pytest-bdd-5.0.0.tar.gz", hash = "sha256:fab7093ed3d5e51ee0c68de093c90e4f40de345bd9a54a188b2991ce2a2a39cf"}, 1401 | {file = "pytest_bdd-5.0.0-py3-none-any.whl", hash = "sha256:c7cf12209606421f61f36b5dc63beccd0c82d29446c0592cf68af2dad0a9761d"}, 1402 | ] 1403 | pytest-cov = [ 1404 | {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, 1405 | {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, 1406 | ] 1407 | pytest-pylint = [ 1408 | {file = "pytest-pylint-0.18.0.tar.gz", hash = "sha256:790c7a8019fab08e59bd3812db1657a01995a975af8b1c6ce95b9aa39d61da27"}, 1409 | {file = "pytest_pylint-0.18.0-py3-none-any.whl", hash = "sha256:b63aaf8b80ff33c8ceaa7f68323ed04102c7790093ccf6bdb261a4c2dc6fd564"}, 1410 | ] 1411 | pytest-randomly = [ 1412 | {file = "pytest-randomly-3.11.0.tar.gz", hash = "sha256:9f013b8c1923130f3d0a286fde56e1fc52cfb3547b8eedf2765c460cee979c7f"}, 1413 | {file = "pytest_randomly-3.11.0-py3-none-any.whl", hash = "sha256:a3c680d2b8150cf766311a80a1f92da64c3dd819045cda834fbf1b0ac4891610"}, 1414 | ] 1415 | pytest-watch = [ 1416 | {file = "pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"}, 1417 | ] 1418 | pyyaml = [ 1419 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 1420 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 1421 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 1422 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 1423 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 1424 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 1425 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 1426 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, 1427 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, 1428 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, 1429 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, 1430 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, 1431 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, 1432 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, 1433 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 1434 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 1435 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 1436 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 1437 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 1438 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 1439 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 1440 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 1441 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 1442 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 1443 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 1444 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 1445 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 1446 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 1447 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 1448 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 1449 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 1450 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 1451 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 1452 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 1453 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 1454 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 1455 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 1456 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 1457 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 1458 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 1459 | ] 1460 | questionary = [ 1461 | {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, 1462 | {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, 1463 | ] 1464 | redis = [ 1465 | {file = "redis-4.1.4-py3-none-any.whl", hash = "sha256:04629f8e42be942c4f7d1812f2094568f04c612865ad19ad3ace3005da70631a"}, 1466 | {file = "redis-4.1.4.tar.gz", hash = "sha256:1d9a0cdf89fdd93f84261733e24f55a7bbd413a9b219fdaf56e3e728ca9a2306"}, 1467 | ] 1468 | requests = [ 1469 | {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, 1470 | {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, 1471 | ] 1472 | setuptools = [ 1473 | {file = "setuptools-65.3.0-py3-none-any.whl", hash = "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82"}, 1474 | {file = "setuptools-65.3.0.tar.gz", hash = "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57"}, 1475 | ] 1476 | six = [ 1477 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1478 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1479 | ] 1480 | sniffio = [ 1481 | {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, 1482 | {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, 1483 | ] 1484 | sortedcontainers = [ 1485 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, 1486 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, 1487 | ] 1488 | starlette = [ 1489 | {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"}, 1490 | {file = "starlette-0.17.1.tar.gz", hash = "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"}, 1491 | ] 1492 | termcolor = [ 1493 | {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, 1494 | ] 1495 | toml = [ 1496 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1497 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1498 | ] 1499 | tomli = [ 1500 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1501 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1502 | ] 1503 | tomlkit = [ 1504 | {file = "tomlkit-0.10.2-py3-none-any.whl", hash = "sha256:905cf92c2111ef80d355708f47ac24ad1b6fc2adc5107455940088c9bbecaedb"}, 1505 | {file = "tomlkit-0.10.2.tar.gz", hash = "sha256:30d54c0b914e595f3d10a87888599eab5321a2a69abc773bbefff51599b72db6"}, 1506 | ] 1507 | tox = [ 1508 | {file = "tox-3.25.0-py2.py3-none-any.whl", hash = "sha256:0805727eb4d6b049de304977dfc9ce315a1938e6619c3ab9f38682bb04662a5a"}, 1509 | {file = "tox-3.25.0.tar.gz", hash = "sha256:37888f3092aa4e9f835fc8cc6dadbaaa0782651c41ef359e3a5743fcb0308160"}, 1510 | ] 1511 | typed-ast = [ 1512 | {file = "typed_ast-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ad3b48cf2b487be140072fb86feff36801487d4abb7382bb1929aaac80638ea"}, 1513 | {file = "typed_ast-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:542cd732351ba8235f20faa0fc7398946fe1a57f2cdb289e5497e1e7f48cfedb"}, 1514 | {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc2c11ae59003d4a26dda637222d9ae924387f96acae9492df663843aefad55"}, 1515 | {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd5df1313915dbd70eaaa88c19030b441742e8b05e6103c631c83b75e0435ccc"}, 1516 | {file = "typed_ast-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:e34f9b9e61333ecb0f7d79c21c28aa5cd63bec15cb7e1310d7d3da6ce886bc9b"}, 1517 | {file = "typed_ast-1.5.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f818c5b81966d4728fec14caa338e30a70dfc3da577984d38f97816c4b3071ec"}, 1518 | {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3042bfc9ca118712c9809201f55355479cfcdc17449f9f8db5e744e9625c6805"}, 1519 | {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4fff9fdcce59dc61ec1b317bdb319f8f4e6b69ebbe61193ae0a60c5f9333dc49"}, 1520 | {file = "typed_ast-1.5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8e0b8528838ffd426fea8d18bde4c73bcb4167218998cc8b9ee0a0f2bfe678a6"}, 1521 | {file = "typed_ast-1.5.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ef1d96ad05a291f5c36895d86d1375c0ee70595b90f6bb5f5fdbee749b146db"}, 1522 | {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed44e81517364cb5ba367e4f68fca01fba42a7a4690d40c07886586ac267d9b9"}, 1523 | {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f60d9de0d087454c91b3999a296d0c4558c1666771e3460621875021bf899af9"}, 1524 | {file = "typed_ast-1.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9e237e74fd321a55c90eee9bc5d44be976979ad38a29bbd734148295c1ce7617"}, 1525 | {file = "typed_ast-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee852185964744987609b40aee1d2eb81502ae63ee8eef614558f96a56c1902d"}, 1526 | {file = "typed_ast-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:27e46cdd01d6c3a0dd8f728b6a938a6751f7bd324817501c15fb056307f918c6"}, 1527 | {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d64dabc6336ddc10373922a146fa2256043b3b43e61f28961caec2a5207c56d5"}, 1528 | {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8cdf91b0c466a6c43f36c1964772918a2c04cfa83df8001ff32a89e357f8eb06"}, 1529 | {file = "typed_ast-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:9cc9e1457e1feb06b075c8ef8aeb046a28ec351b1958b42c7c31c989c841403a"}, 1530 | {file = "typed_ast-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e20d196815eeffb3d76b75223e8ffed124e65ee62097e4e73afb5fec6b993e7a"}, 1531 | {file = "typed_ast-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:37e5349d1d5de2f4763d534ccb26809d1c24b180a477659a12c4bde9dd677d74"}, 1532 | {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f1a27592fac87daa4e3f16538713d705599b0a27dfe25518b80b6b017f0a6d"}, 1533 | {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8831479695eadc8b5ffed06fdfb3e424adc37962a75925668deeb503f446c0a3"}, 1534 | {file = "typed_ast-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:20d5118e494478ef2d3a2702d964dae830aedd7b4d3b626d003eea526be18718"}, 1535 | {file = "typed_ast-1.5.3.tar.gz", hash = "sha256:27f25232e2dd0edfe1f019d6bfaaf11e86e657d9bdb7b0956db95f560cceb2b3"}, 1536 | ] 1537 | typing-extensions = [ 1538 | {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, 1539 | {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, 1540 | ] 1541 | urllib3 = [ 1542 | {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, 1543 | {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, 1544 | ] 1545 | virtualenv = [ 1546 | {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"}, 1547 | {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"}, 1548 | ] 1549 | watchdog = [ 1550 | {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:177bae28ca723bc00846466016d34f8c1d6a621383b6caca86745918d55c7383"}, 1551 | {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d1cf7dfd747dec519486a98ef16097e6c480934ef115b16f18adb341df747a4"}, 1552 | {file = "watchdog-2.1.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f14ce6adea2af1bba495acdde0e510aecaeb13b33f7bd2f6324e551b26688ca"}, 1553 | {file = "watchdog-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4d0e98ac2e8dd803a56f4e10438b33a2d40390a72750cff4939b4b274e7906fa"}, 1554 | {file = "watchdog-2.1.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:81982c7884aac75017a6ecc72f1a4fedbae04181a8665a34afce9539fc1b3fab"}, 1555 | {file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0b4a1fe6201c6e5a1926f5767b8664b45f0fcb429b62564a41f490ff1ce1dc7a"}, 1556 | {file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6e6ae29b72977f2e1ee3d0b760d7ee47896cb53e831cbeede3e64485e5633cc8"}, 1557 | {file = "watchdog-2.1.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b9777664848160449e5b4260e0b7bc1ae0f6f4992a8b285db4ec1ef119ffa0e2"}, 1558 | {file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:19b36d436578eb437e029c6b838e732ed08054956366f6dd11875434a62d2b99"}, 1559 | {file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b61acffaf5cd5d664af555c0850f9747cc5f2baf71e54bbac164c58398d6ca7b"}, 1560 | {file = "watchdog-2.1.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1e877c70245424b06c41ac258023ea4bd0c8e4ff15d7c1368f17cd0ae6e351dd"}, 1561 | {file = "watchdog-2.1.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d802d65262a560278cf1a65ef7cae4e2bc7ecfe19e5451349e4c67e23c9dc420"}, 1562 | {file = "watchdog-2.1.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b3750ee5399e6e9c69eae8b125092b871ee9e2fcbd657a92747aea28f9056a5c"}, 1563 | {file = "watchdog-2.1.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:ed6d9aad09a2a948572224663ab00f8975fae242aa540509737bb4507133fa2d"}, 1564 | {file = "watchdog-2.1.7-py3-none-manylinux2014_armv7l.whl", hash = "sha256:b26e13e8008dcaea6a909e91d39b629a39635d1a8a7239dd35327c74f4388601"}, 1565 | {file = "watchdog-2.1.7-py3-none-manylinux2014_i686.whl", hash = "sha256:0908bb50f6f7de54d5d31ec3da1654cb7287c6b87bce371954561e6de379d690"}, 1566 | {file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64.whl", hash = "sha256:bdcbf75580bf4b960fb659bbccd00123d83119619195f42d721e002c1621602f"}, 1567 | {file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:81a5861d0158a7e55fe149335fb2bbfa6f48cbcbd149b52dbe2cd9a544034bbd"}, 1568 | {file = "watchdog-2.1.7-py3-none-manylinux2014_s390x.whl", hash = "sha256:03b43d583df0f18782a0431b6e9e9965c5b3f7cf8ec36a00b930def67942c385"}, 1569 | {file = "watchdog-2.1.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ae934e34c11aa8296c18f70bf66ed60e9870fcdb4cc19129a04ca83ab23e7055"}, 1570 | {file = "watchdog-2.1.7-py3-none-win32.whl", hash = "sha256:49639865e3db4be032a96695c98ac09eed39bbb43fe876bb217da8f8101689a6"}, 1571 | {file = "watchdog-2.1.7-py3-none-win_amd64.whl", hash = "sha256:340b875aecf4b0e6672076a6f05cfce6686935559bb6d34cebedee04126a9566"}, 1572 | {file = "watchdog-2.1.7-py3-none-win_ia64.whl", hash = "sha256:351e09b6d9374d5bcb947e6ac47a608ec25b9d70583e9db00b2fcdb97b00b572"}, 1573 | {file = "watchdog-2.1.7.tar.gz", hash = "sha256:3fd47815353be9c44eebc94cc28fe26b2b0c5bd889dafc4a5a7cbdf924143480"}, 1574 | ] 1575 | wcwidth = [ 1576 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 1577 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 1578 | ] 1579 | wrapt = [ 1580 | {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, 1581 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, 1582 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, 1583 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, 1584 | {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, 1585 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, 1586 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, 1587 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, 1588 | {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, 1589 | {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, 1590 | {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, 1591 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, 1592 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, 1593 | {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, 1594 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, 1595 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, 1596 | {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, 1597 | {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, 1598 | {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, 1599 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, 1600 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, 1601 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, 1602 | {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, 1603 | {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, 1604 | {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, 1605 | {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, 1606 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, 1607 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, 1608 | {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, 1609 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, 1610 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, 1611 | {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, 1612 | {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, 1613 | {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, 1614 | {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, 1615 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, 1616 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, 1617 | {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, 1618 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, 1619 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, 1620 | {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, 1621 | {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, 1622 | {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, 1623 | {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, 1624 | {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, 1625 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, 1626 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, 1627 | {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, 1628 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, 1629 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, 1630 | {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, 1631 | {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, 1632 | {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, 1633 | {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, 1634 | {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, 1635 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, 1636 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, 1637 | {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, 1638 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, 1639 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, 1640 | {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, 1641 | {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, 1642 | {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, 1643 | {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, 1644 | ] 1645 | zipp = [ 1646 | {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, 1647 | {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, 1648 | ] 1649 | --------------------------------------------------------------------------------