├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── asyncio_redis_rate_limit ├── __init__.py ├── compat.py └── py.typed ├── docker-compose.yml ├── poetry.lock ├── pyproject.toml ├── setup.cfg └── tests ├── test_examples.py ├── test_multiprocess_aioredis.py └── test_multiprocess_redis.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # Check http://editorconfig.org for more information 2 | # This is the main config file for this project: 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | 13 | [*.{py, pyi}] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [Makefile] 18 | indent_style = tab 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "02:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | time: "02:00" 14 | open-pull-requests-limit: 10 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 24 | redis-image: ['redis:7.4-alpine'] 25 | env-type: ['redis'] 26 | 27 | include: 28 | - python-version: '3.10' 29 | env-type: 'aioredis' 30 | redis-image: 'redis:7.0-alpine' 31 | - python-version: '3.9' 32 | env-type: 'dev' 33 | redis-image: 'redis:7.0-alpine' 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Set up Python ${{ matrix.python-version }} 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: ${{ matrix.python-version }} 41 | 42 | - name: Install poetry 43 | run: | 44 | curl -sSL "https://install.python-poetry.org" | python 45 | # Adding `poetry` to `$PATH`: 46 | echo "$HOME/.poetry/bin" >> $GITHUB_PATH 47 | 48 | - name: Install dependencies 49 | run: | 50 | poetry config virtualenvs.in-project true 51 | poetry run pip install -U pip 52 | poetry install --extras=${{ matrix.env-type }} 53 | 54 | - name: Pull and build docker-compose services 55 | run: | 56 | docker compose pull 57 | docker compose up --detach 58 | env: 59 | REDIS_IMAGE: ${{ matrix.redis-image }} 60 | 61 | - name: Run checks 62 | run: make test 63 | 64 | - name: Run doctests 65 | if: ${{ matrix.env-type == 'dev' }} 66 | run: | 67 | poetry run pytest \ 68 | -p no:cov -o addopts="" \ 69 | --doctest-modules --doctest-glob='*.md' \ 70 | asyncio_redis_rate_limit README.md 71 | 72 | # Upload coverage to codecov: https://codecov.io/ 73 | - name: Upload coverage to Codecov 74 | uses: codecov/codecov-action@v5 75 | if: ${{ matrix.python-version == '3.12' && matrix.env-type == 'dev' }} 76 | with: 77 | file: ./coverage.xml 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | #### macos #### 3 | # General 4 | *.DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | .com.apple.timemachine.donotpresent 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | #### linux #### 31 | *~ 32 | 33 | # temporary files which can be created if a process still has a handle open of a deleted file 34 | .fuse_hidden* 35 | 36 | # KDE directory preferences 37 | .directory 38 | 39 | # Linux trash folder which might appear on any partition or disk 40 | .Trash-* 41 | 42 | # .nfs files are created when an open file is removed but is still being accessed 43 | .nfs* 44 | #### windows #### 45 | # Windows thumbnail cache files 46 | Thumbs.db 47 | ehthumbs.db 48 | ehthumbs_vista.db 49 | 50 | # Dump file 51 | *.stackdump 52 | 53 | # Folder config file 54 | Desktop.ini 55 | 56 | # Recycle Bin used on file shares 57 | $RECYCLE.BIN/ 58 | 59 | # Windows Installer files 60 | *.cab 61 | *.msi 62 | *.msm 63 | *.msp 64 | 65 | # Windows shortcuts 66 | *.lnk 67 | #### python #### 68 | # Byte-compiled / optimized / DLL files 69 | __pycache__/ 70 | *.py[cod] 71 | *$py.class 72 | 73 | # C extensions 74 | *.so 75 | 76 | # Distribution / packaging 77 | .Python 78 | build/ 79 | develop-eggs/ 80 | dist/ 81 | downloads/ 82 | eggs/ 83 | .eggs/ 84 | lib/ 85 | lib64/ 86 | parts/ 87 | sdist/ 88 | var/ 89 | wheels/ 90 | *.egg-info/ 91 | .installed.cfg 92 | *.egg 93 | 94 | # PyInstaller 95 | # Usually these files are written by a python script from a template 96 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 97 | *.manifest 98 | *.spec 99 | 100 | # Installer logs 101 | pip-log.txt 102 | pip-delete-this-directory.txt 103 | 104 | # Unit test / coverage reports 105 | htmlcov/ 106 | .tox/ 107 | .coverage 108 | .coverage.* 109 | .cache 110 | nosetests.xml 111 | coverage.xml 112 | *.cover 113 | .hypothesis/ 114 | 115 | # Translations 116 | *.mo 117 | *.pot 118 | 119 | # Django stuff: 120 | *.log 121 | local_settings.py 122 | 123 | # Flask stuff: 124 | instance/ 125 | .webassets-cache 126 | 127 | # Scrapy stuff: 128 | .scrapy 129 | 130 | # Sphinx documentation 131 | docs/_build/ 132 | 133 | # PyBuilder 134 | target/ 135 | 136 | # Jupyter Notebook 137 | .ipynb_checkpoints 138 | 139 | # celery beat schedule file 140 | celerybeat-schedule 141 | 142 | # SageMath parsed files 143 | *.sage.py 144 | 145 | # Environments 146 | .env 147 | .venv 148 | env/ 149 | venv/ 150 | ENV/ 151 | 152 | # Spyder project settings 153 | .spyderproject 154 | .spyproject 155 | 156 | # Rope project settings 157 | .ropeproject 158 | 159 | # mkdocs documentation 160 | /site 161 | 162 | # mypy 163 | .mypy_cache/ 164 | 165 | #### jetbrains #### 166 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 167 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 168 | 169 | # User-specific stuff: 170 | .idea/**/workspace.xml 171 | .idea/**/tasks.xml 172 | .idea/dictionaries 173 | 174 | # Sensitive or high-churn files: 175 | .idea/**/dataSources/ 176 | .idea/**/dataSources.ids 177 | .idea/**/dataSources.xml 178 | .idea/**/dataSources.local.xml 179 | .idea/**/sqlDataSources.xml 180 | .idea/**/dynamic.xml 181 | .idea/**/uiDesigner.xml 182 | 183 | # Gradle: 184 | .idea/**/gradle.xml 185 | .idea/**/libraries 186 | 187 | # CMake 188 | cmake-build-debug/ 189 | 190 | # Mongo Explorer plugin: 191 | .idea/**/mongoSettings.xml 192 | 193 | ## File-based project format: 194 | *.iws 195 | 196 | ## Plugin-specific files: 197 | 198 | # IntelliJ 199 | /out/ 200 | 201 | # mpeltonen/sbt-idea plugin 202 | .idea_modules/ 203 | 204 | # JIRA plugin 205 | atlassian-ide-plugin.xml 206 | 207 | # Cursive Clojure plugin 208 | .idea/replstate.xml 209 | 210 | # Crashlytics plugin (for Android Studio and IntelliJ) 211 | com_crashlytics_export_strings.xml 212 | crashlytics.properties 213 | crashlytics-build.properties 214 | fabric.properties 215 | 216 | # pyenv 217 | .python-version 218 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version history 2 | 3 | We follow [Semantic Versions](https://semver.org/). 4 | 5 | ## WIP 6 | 7 | ### Features 8 | 9 | - *breaking*: Removes `python3.8` support 10 | - Adds `python3.12` support 11 | 12 | 13 | ## Version 1.0.0 14 | 15 | ### Features 16 | 17 | - *breaking*: Removes `python3.7` support 18 | - Adds `python3.11` support 19 | 20 | ### Misc 21 | 22 | - Updates multiple deps 23 | 24 | 25 | ## Version 0.2.0 26 | 27 | ### Features 28 | 29 | - Adds support for `aioredis` 30 | 31 | 32 | ## Version 0.1.1 33 | 34 | ### Misc 35 | 36 | - Fixes all names to `asyncio-redis-rate-limit` 37 | from `aio-redis-rate-limit` in docs 38 | 39 | 40 | ## Version 0.1.0 41 | 42 | - Initial release 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | 4 | ## Dependencies 5 | 6 | We use [poetry](https://github.com/python-poetry/poetry) to manage the dependencies. 7 | 8 | To install them you would need to run `install` command: 9 | 10 | ```bash 11 | poetry install 12 | ``` 13 | 14 | To activate your `virtualenv` run `poetry shell`. 15 | 16 | 17 | ## One magic command 18 | 19 | Run `make test` to run everything we have! 20 | 21 | 22 | ## Tests 23 | 24 | We use `pytest` and `flake8` for quality control. 25 | We also use [wemake_python_styleguide](https://github.com/wemake-services/wemake-python-styleguide) to enforce the code quality. 26 | 27 | To run all tests: 28 | 29 | ```bash 30 | pytest 31 | ``` 32 | 33 | To run linting: 34 | 35 | ```bash 36 | flake8 . 37 | ``` 38 | Keep in mind: default virtual environment folder excluded by flake8 style checking is `.venv`. 39 | If you want to customize this parameter, you should do this in `setup.cfg`. 40 | These steps are mandatory during the CI. 41 | 42 | 43 | ## Type checks 44 | 45 | We use `mypy` to run type checks on our code. 46 | To use it: 47 | 48 | ```bash 49 | mypy . 50 | ``` 51 | 52 | This step is mandatory during the CI. 53 | 54 | 55 | ## Submitting your code 56 | 57 | We use [trunk based](https://trunkbaseddevelopment.com/) 58 | development (we also sometimes call it `wemake-git-flow`). 59 | 60 | What the point of this method? 61 | 62 | 1. We use protected `master` branch, 63 | so the only way to push your code is via pull request 64 | 2. We use issue branches: to implement a new feature or to fix a bug 65 | create a new branch named `issue-$TASKNUMBER` 66 | 3. Then create a pull request to `master` branch 67 | 4. We use `git tag`s to make releases, so we can track what has changed 68 | since the latest release 69 | 70 | So, this way we achieve an easy and scalable development process 71 | which frees us from merging hell and long-living branches. 72 | 73 | In this method, the latest version of the app is always in the `master` branch. 74 | 75 | ### Before submitting 76 | 77 | Before submitting your code please do the following steps: 78 | 79 | 1. Run `pytest` to make sure everything was working before 80 | 2. Add any changes you want 81 | 3. Add tests for the new changes 82 | 4. Edit documentation if you have changed something significant 83 | 5. Update `CHANGELOG.md` with a quick summary of your changes 84 | 6. Run `pytest` again to make sure it is still working 85 | 7. Run `mypy` to ensure that types are correct 86 | 8. Run `flake8` to ensure that style is correct 87 | 9. Run `doc8` to ensure that docs are correct 88 | 89 | 90 | ## Other help 91 | 92 | You can contribute by spreading a word about this library. 93 | It would also be a huge contribution to write 94 | a short article on how you are using this project. 95 | You can also share your best practices with us. 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | Copyright (c) 2022 wemake-services 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, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL:=/usr/bin/env bash 2 | 3 | .PHONY: lint 4 | lint: 5 | poetry run mypy . 6 | poetry run flake8 . 7 | 8 | .PHONY: unit 9 | unit: 10 | poetry run pytest 11 | 12 | .PHONY: package 13 | package: 14 | poetry check 15 | poetry run pip check 16 | 17 | .PHONY: test 18 | test: lint package unit 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # asyncio-redis-rate-limit 2 | 3 | [![wemake.services](https://img.shields.io/badge/%20-wemake.services-green.svg?label=%20&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC%2FxhBQAAAAFzUkdCAK7OHOkAAAAbUExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP%2F%2F%2F5TvxDIAAAAIdFJOUwAjRA8xXANAL%2Bv0SAAAADNJREFUGNNjYCAIOJjRBdBFWMkVQeGzcHAwksJnAPPZGOGAASzPzAEHEGVsLExQwE7YswCb7AFZSF3bbAAAAABJRU5ErkJggg%3D%3D)](https://wemake-services.github.io) 4 | [![Build Status](https://github.com/wemake-services/asyncio-redis-rate-limit/workflows/test/badge.svg?branch=master&event=push)](https://github.com/wemake-services/asyncio-redis-rate-limit/actions?query=workflow%3Atest) 5 | [![codecov](https://codecov.io/gh/wemake-services/asyncio-redis-rate-limit/branch/master/graph/badge.svg)](https://codecov.io/gh/wemake-services/asyncio-redis-rate-limit) 6 | [![Python Version](https://img.shields.io/pypi/pyversions/asyncio-redis-rate-limit.svg)](https://pypi.org/project/asyncio-redis-rate-limit/) 7 | [![wemake-python-styleguide](https://img.shields.io/badge/style-wemake-000000.svg)](https://github.com/wemake-services/wemake-python-styleguide) 8 | 9 | Rate limiter for async functions using Redis as a backend. 10 | 11 | 12 | ## Features 13 | 14 | - Small and simple 15 | - Can be used as a decorator or as a context manager 16 | - Can be used for both clients and servers 17 | - Works with `asyncio` 18 | - Works with any amount of processes 19 | - Works with both [`redis.asyncio.client.Redis`](https://redis-py.readthedocs.io/en/stable/examples/asyncio_examples.html) and [`aioredis`](https://github.com/aio-libs/aioredis-py) 20 | - Free of race-conditions (hopefully!) 21 | - Supports `redis` since `7.0` 22 | - Fully typed with annotations and checked with mypy, [PEP561 compatible](https://www.python.org/dev/peps/pep-0561/) 23 | 24 | 25 | ## Installation 26 | 27 | ```bash 28 | pip install asyncio-redis-rate-limit 29 | ``` 30 | 31 | Extras available: 32 | - `pip install asyncio-redis-rate-limit[redis]` 33 | - `pip install asyncio-redis-rate-limit[aioredis]` (for python versions `<3.11`) 34 | 35 | 36 | ## Example 37 | 38 | As a decorator: 39 | 40 | ```python 41 | >>> from asyncio_redis_rate_limit import rate_limit, RateSpec 42 | >>> from redis.asyncio import Redis as AsyncRedis # pip install redis 43 | 44 | >>> redis = AsyncRedis.from_url('redis://localhost:6379') 45 | 46 | >>> @rate_limit( 47 | ... rate_spec=RateSpec(requests=1200, seconds=60), 48 | ... backend=redis, 49 | ... ) 50 | ... async def request() -> ...: 51 | ... ... # Do something useful! Call this function as usual. 52 | 53 | ``` 54 | 55 | Or as a context manager: 56 | 57 | ```python 58 | >>> from asyncio_redis_rate_limit import RateLimiter, RateSpec 59 | >>> from redis.asyncio import Redis as AsyncRedis # pip install redis 60 | 61 | >>> redis = AsyncRedis.from_url('redis://localhost:6379') 62 | 63 | >>> async def request() -> ...: 64 | ... async with RateLimiter( 65 | ... unique_key='api-name.com', 66 | ... backend=redis, 67 | ... rate_spec=RateSpec(requests=5, seconds=1), 68 | ... ): 69 | ... ... # Do the request itself. 70 | 71 | ``` 72 | 73 | 74 | ## License 75 | 76 | [MIT](https://github.com/wemake-services/asyncio-redis-rate-limit/blob/master/LICENSE) 77 | 78 | 79 | ## Credits 80 | 81 | This project was generated with [`wemake-python-package`](https://github.com/wemake-services/wemake-python-package). Current template version is: [1d63652fbb33ebe2f6d932f511b7f529a4ce2d2a](https://github.com/wemake-services/wemake-python-package/tree/1d63652fbb33ebe2f6d932f511b7f529a4ce2d2a). See what is [updated](https://github.com/wemake-services/wemake-python-package/compare/1d63652fbb33ebe2f6d932f511b7f529a4ce2d2a...master) since then. 82 | -------------------------------------------------------------------------------- /asyncio_redis_rate_limit/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import hashlib 3 | from functools import wraps 4 | from types import TracebackType 5 | from typing import Awaitable, Callable, NamedTuple, Optional, Type, TypeVar 6 | 7 | from typing_extensions import ParamSpec, TypeAlias, final 8 | 9 | from asyncio_redis_rate_limit.compat import ( 10 | AnyPipeline, 11 | AnyRedis, 12 | pipeline_expire, 13 | ) 14 | 15 | #: These aliases makes our code more readable. 16 | _Seconds: TypeAlias = int 17 | 18 | _ResultT = TypeVar('_ResultT') 19 | _ParamsT = ParamSpec('_ParamsT') 20 | 21 | _CoroutineFunction: TypeAlias = Callable[_ParamsT, Awaitable[_ResultT]] 22 | 23 | _RateLimiterT = TypeVar('_RateLimiterT', bound='RateLimiter') 24 | 25 | 26 | @final 27 | class RateLimitError(Exception): 28 | """We raise this error when rate limit is hit.""" 29 | 30 | 31 | @final 32 | class RateSpec(NamedTuple): 33 | """ 34 | Specifies the amount of requests can be made in the time frame in seconds. 35 | 36 | It is much nicier than using a custom string format like ``100/1s``. 37 | """ 38 | 39 | requests: int 40 | seconds: _Seconds 41 | 42 | 43 | class RateLimiter: 44 | """Implements rate limiting.""" 45 | 46 | __slots__ = ( 47 | '_unique_key', 48 | '_rate_spec', 49 | '_backend', 50 | '_cache_prefix', 51 | '_lock', 52 | ) 53 | 54 | def __init__( 55 | self, 56 | unique_key: str, 57 | rate_spec: RateSpec, 58 | backend: AnyRedis, 59 | *, 60 | cache_prefix: str, 61 | ) -> None: 62 | """In the future other backends might be supported as well.""" 63 | self._unique_key = unique_key 64 | self._rate_spec = rate_spec 65 | self._backend = backend 66 | self._cache_prefix = cache_prefix 67 | self._lock = asyncio.Lock() 68 | 69 | async def __aenter__(self: _RateLimiterT) -> _RateLimiterT: 70 | """ 71 | Async context manager API. 72 | 73 | Before this object will be used, we call ``self._acquire`` to be sure 74 | that we can actually make any actions in this time frame. 75 | """ 76 | await self._acquire() 77 | return self 78 | 79 | async def __aexit__( 80 | self, 81 | exc_type: Optional[Type[BaseException]], 82 | exc: Optional[BaseException], 83 | tb: Optional[TracebackType], 84 | ) -> None: 85 | """Do nothing. We need this to ``__aenter__`` to work.""" 86 | 87 | # Private API: 88 | 89 | async def _acquire(self) -> None: 90 | cache_key = self._make_cache_key( 91 | unique_key=self._unique_key, 92 | rate_spec=self._rate_spec, 93 | cache_prefix=self._cache_prefix, 94 | ) 95 | pipeline = self._backend.pipeline() 96 | 97 | async with self._lock: 98 | current_rate = await self._run_pipeline(cache_key, pipeline) 99 | # This looks like a coverage error on 3.10: 100 | if current_rate > self._rate_spec.requests: # pragma: no cover 101 | raise RateLimitError('Rate limit is hit', current_rate) 102 | 103 | async def _run_pipeline( 104 | self, 105 | cache_key: str, 106 | pipeline: AnyPipeline, 107 | ) -> int: 108 | # https://redis.io/commands/incr/#pattern-rate-limiter-1 109 | current_rate, _ = await pipeline_expire( 110 | pipeline.incr(cache_key), 111 | cache_key, 112 | self._rate_spec.seconds, 113 | ).execute() 114 | return current_rate # type: ignore[no-any-return] 115 | 116 | def _make_cache_key( 117 | self, 118 | unique_key: str, 119 | rate_spec: RateSpec, 120 | cache_prefix: str, 121 | ) -> str: 122 | parts = ''.join([unique_key, str(rate_spec)]) 123 | return cache_prefix + hashlib.md5( # noqa: S303, S324 124 | parts.encode('utf-8'), 125 | ).hexdigest() 126 | 127 | 128 | def rate_limit( # noqa: WPS320 129 | rate_spec: RateSpec, 130 | backend: AnyRedis, 131 | *, 132 | cache_prefix: str = 'aio-rate-limit', 133 | ) -> Callable[ 134 | [_CoroutineFunction[_ParamsT, _ResultT]], 135 | _CoroutineFunction[_ParamsT, _ResultT], 136 | ]: 137 | """ 138 | Rate limits a function. 139 | 140 | Code example: 141 | 142 | .. code:: python 143 | 144 | >>> from asyncio_redis_rate_limit import rate_limit, RateSpec 145 | >>> from redis.asyncio import Redis as AsyncRedis 146 | 147 | >>> redis = AsyncRedis.from_url('redis://localhost:6379') 148 | 149 | >>> @rate_limit( 150 | ... rate_spec=RateSpec(requests=1200, seconds=60), 151 | ... backend=redis, 152 | ... ) 153 | ... async def request() -> int: 154 | ... ... # Do something 155 | 156 | """ 157 | def decorator( 158 | function: _CoroutineFunction[_ParamsT, _ResultT], 159 | ) -> _CoroutineFunction[_ParamsT, _ResultT]: 160 | @wraps(function) 161 | async def factory( 162 | *args: _ParamsT.args, 163 | **kwargs: _ParamsT.kwargs, 164 | ) -> _ResultT: 165 | async with RateLimiter( 166 | unique_key=function.__qualname__, 167 | backend=backend, 168 | rate_spec=rate_spec, 169 | cache_prefix=cache_prefix, 170 | ): 171 | return await function(*args, **kwargs) 172 | return factory 173 | return decorator 174 | -------------------------------------------------------------------------------- /asyncio_redis_rate_limit/compat.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Generic, TypeVar, Union 2 | 3 | from typing_extensions import TypeAlias 4 | 5 | _EmptyType = TypeVar('_EmptyType') 6 | 7 | try: # noqa: WPS229 # pragma: no cover 8 | from redis.asyncio.client import Pipeline as _AsyncPipeline # noqa: WPS433 9 | from redis.asyncio.client import Redis as _AsyncRedis # noqa: WPS433 10 | 11 | HAS_REDIS = True 12 | except ImportError: 13 | class _AsyncPipeline( # type: ignore # noqa: WPS306, WPS440 14 | Generic[_EmptyType], 15 | ): 16 | """Fallback pipeline type if `redis` is not installed.""" 17 | 18 | class _AsyncRedis( # type: ignore # noqa: WPS306, WPS440 19 | Generic[_EmptyType], 20 | ): 21 | """Fallback redis type if `redis` is not installed.""" 22 | 23 | HAS_REDIS = False 24 | 25 | try: # noqa: WPS229 # pragma: no cover 26 | from aioredis.client import Pipeline as _AIOPipeline # noqa: WPS433 27 | from aioredis.client import Redis as _AIORedis # noqa: WPS433 28 | 29 | HAS_AIOREDIS = True 30 | except ImportError: 31 | class _AIOPipeline: # type: ignore # noqa: WPS306, WPS440 32 | """Fallback pipeline type if `aioredis` is not installed.""" 33 | 34 | class _AIORedis: # type: ignore # noqa: WPS306, WPS440 35 | """Fallback redis type if `aioredis` is not installed.""" 36 | 37 | HAS_AIOREDIS = False 38 | 39 | AnyPipeline: TypeAlias = Union['_AsyncPipeline[Any]', _AIOPipeline] 40 | AnyRedis: TypeAlias = Union['_AsyncRedis[Any]', _AIORedis] 41 | 42 | 43 | def pipeline_expire( 44 | pipeline: Any, 45 | cache_key: str, 46 | seconds: int, 47 | ) -> AnyPipeline: 48 | """Compatibility mode for `.expire(..., nx=True)` command.""" 49 | if isinstance(pipeline, _AsyncPipeline): 50 | return pipeline.expire(cache_key, seconds, nx=True) # type: ignore 51 | # `aioredis` somehow does not have this boolean argument in `.expire`, 52 | # so, we use `EXPIRE` directly with `NX` flag. 53 | return pipeline.execute_command( # type: ignore 54 | 'EXPIRE', 55 | cache_key, 56 | seconds, 57 | 'NX', 58 | ) 59 | -------------------------------------------------------------------------------- /asyncio_redis_rate_limit/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wemake-services/asyncio-redis-rate-limit/1db3bfb1c5729be1bc8177112163588951fdb007/asyncio_redis_rate_limit/py.typed -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | version: '3.8' 4 | 5 | services: 6 | redis: 7 | image: ${REDIS_IMAGE:-'redis:alpine'} 8 | ports: 9 | - "6379:6379" 10 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "aioredis" 5 | version = "2.0.1" 6 | description = "asyncio (PEP 3156) Redis support" 7 | optional = true 8 | python-versions = ">=3.6" 9 | groups = ["main"] 10 | markers = "python_version < \"3.11\" and (extra == \"aioredis\" or extra == \"dev\")" 11 | files = [ 12 | {file = "aioredis-2.0.1-py3-none-any.whl", hash = "sha256:9ac0d0b3b485d293b8ca1987e6de8658d7dafcca1cddfcd1d506cae8cdebfdd6"}, 13 | {file = "aioredis-2.0.1.tar.gz", hash = "sha256:eaa51aaf993f2d71f54b70527c440437ba65340588afeb786cd87c55c89cd98e"}, 14 | ] 15 | 16 | [package.dependencies] 17 | async-timeout = "*" 18 | typing-extensions = "*" 19 | 20 | [package.extras] 21 | hiredis = ["hiredis (>=1.0) ; implementation_name == \"cpython\""] 22 | 23 | [[package]] 24 | name = "astor" 25 | version = "0.8.1" 26 | description = "Read/rewrite/write Python ASTs" 27 | optional = false 28 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 29 | groups = ["test"] 30 | files = [ 31 | {file = "astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5"}, 32 | {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"}, 33 | ] 34 | 35 | [[package]] 36 | name = "async-timeout" 37 | version = "4.0.3" 38 | description = "Timeout context manager for asyncio programs" 39 | optional = true 40 | python-versions = ">=3.7" 41 | groups = ["main"] 42 | markers = "python_version < \"3.11\" and (extra == \"aioredis\" or extra == \"dev\") or (extra == \"dev\" or extra == \"redis\") and python_full_version < \"3.11.3\"" 43 | files = [ 44 | {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, 45 | {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, 46 | ] 47 | 48 | [[package]] 49 | name = "attrs" 50 | version = "24.1.0" 51 | description = "Classes Without Boilerplate" 52 | optional = false 53 | python-versions = ">=3.7" 54 | groups = ["test"] 55 | files = [ 56 | {file = "attrs-24.1.0-py3-none-any.whl", hash = "sha256:377b47448cb61fea38533f671fba0d0f8a96fd58facd4dc518e3dac9dbea0905"}, 57 | {file = "attrs-24.1.0.tar.gz", hash = "sha256:adbdec84af72d38be7628e353a09b6a6790d15cd71819f6e9d7b0faa8a125745"}, 58 | ] 59 | 60 | [package.extras] 61 | benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] 62 | cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] 63 | dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] 64 | docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] 65 | tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] 66 | tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\""] 67 | 68 | [[package]] 69 | name = "autorepr" 70 | version = "0.3.0" 71 | description = "Makes civilized __repr__, __str__, and __unicode__ methods" 72 | optional = false 73 | python-versions = "*" 74 | groups = ["test"] 75 | files = [ 76 | {file = "autorepr-0.3.0-py2-none-any.whl", hash = "sha256:c34567e4073630feb52d9c788fc198085e9e9de4817e3b93b7c4c534fc689f11"}, 77 | {file = "autorepr-0.3.0-py2.py3-none-any.whl", hash = "sha256:1d9010d14fb325d3961e3aa73692685563f97d6ba4a2f0f735329fb37422599c"}, 78 | {file = "autorepr-0.3.0.tar.gz", hash = "sha256:ef770b84793d5433e6bb893054973b8c7ce6b487274f9c3f734f678cae11e85e"}, 79 | ] 80 | 81 | [[package]] 82 | name = "bandit" 83 | version = "1.7.9" 84 | description = "Security oriented static analyser for python code." 85 | optional = false 86 | python-versions = ">=3.8" 87 | groups = ["test"] 88 | files = [ 89 | {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, 90 | {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, 91 | ] 92 | 93 | [package.dependencies] 94 | colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} 95 | PyYAML = ">=5.3.1" 96 | rich = "*" 97 | stevedore = ">=1.20.0" 98 | 99 | [package.extras] 100 | baseline = ["GitPython (>=3.1.30)"] 101 | sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] 102 | test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] 103 | toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] 104 | yaml = ["PyYAML"] 105 | 106 | [[package]] 107 | name = "cattrs" 108 | version = "23.2.3" 109 | description = "Composable complex class support for attrs and dataclasses." 110 | optional = false 111 | python-versions = ">=3.8" 112 | groups = ["test"] 113 | files = [ 114 | {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"}, 115 | {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"}, 116 | ] 117 | 118 | [package.dependencies] 119 | attrs = ">=23.1.0" 120 | exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} 121 | typing-extensions = {version = ">=4.1.0,<4.6.3 || >4.6.3", markers = "python_version < \"3.11\""} 122 | 123 | [package.extras] 124 | bson = ["pymongo (>=4.4.0)"] 125 | cbor2 = ["cbor2 (>=5.4.6)"] 126 | msgpack = ["msgpack (>=1.0.5)"] 127 | orjson = ["orjson (>=3.9.2) ; implementation_name == \"cpython\""] 128 | pyyaml = ["pyyaml (>=6.0)"] 129 | tomlkit = ["tomlkit (>=0.11.8)"] 130 | ujson = ["ujson (>=5.7.0)"] 131 | 132 | [[package]] 133 | name = "certifi" 134 | version = "2024.7.4" 135 | description = "Python package for providing Mozilla's CA Bundle." 136 | optional = false 137 | python-versions = ">=3.6" 138 | groups = ["test"] 139 | files = [ 140 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 141 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 142 | ] 143 | 144 | [[package]] 145 | name = "cffi" 146 | version = "1.16.0" 147 | description = "Foreign Function Interface for Python calling C code." 148 | optional = false 149 | python-versions = ">=3.8" 150 | groups = ["test"] 151 | markers = "platform_python_implementation != \"PyPy\"" 152 | files = [ 153 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 154 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 155 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 156 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 157 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 158 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 159 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 160 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 161 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 162 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 163 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 164 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 165 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 166 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 167 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 168 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 169 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 170 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 171 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 172 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 173 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 174 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 175 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 176 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 177 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 178 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 179 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 180 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 181 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 182 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 183 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 184 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 185 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 186 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 187 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 188 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 189 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 190 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 191 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 192 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 193 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 194 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 195 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 196 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 197 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 198 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 199 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 200 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 201 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 202 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 203 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 204 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 205 | ] 206 | 207 | [package.dependencies] 208 | pycparser = "*" 209 | 210 | [[package]] 211 | name = "charset-normalizer" 212 | version = "3.3.2" 213 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 214 | optional = false 215 | python-versions = ">=3.7.0" 216 | groups = ["test"] 217 | files = [ 218 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 219 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 220 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 221 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 222 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 223 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 224 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 225 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 226 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 227 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 228 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 229 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 230 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 231 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 232 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 233 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 234 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 235 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 236 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 237 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 238 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 239 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 240 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 241 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 242 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 243 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 244 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 245 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 246 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 247 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 248 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 249 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 250 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 251 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 252 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 253 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 254 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 255 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 256 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 257 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 258 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 259 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 260 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 261 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 262 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 263 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 264 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 265 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 266 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 267 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 268 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 269 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 270 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 271 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 272 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 273 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 274 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 275 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 276 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 277 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 278 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 279 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 280 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 281 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 282 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 283 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 284 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 285 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 286 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 287 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 288 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 289 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 290 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 291 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 292 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 293 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 294 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 295 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 296 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 297 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 298 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 299 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 300 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 301 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 302 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 303 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 304 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 305 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 306 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 307 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 308 | ] 309 | 310 | [[package]] 311 | name = "click" 312 | version = "8.1.7" 313 | description = "Composable command line interface toolkit" 314 | optional = false 315 | python-versions = ">=3.7" 316 | groups = ["test"] 317 | files = [ 318 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 319 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 320 | ] 321 | 322 | [package.dependencies] 323 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 324 | 325 | [[package]] 326 | name = "colorama" 327 | version = "0.4.6" 328 | description = "Cross-platform colored terminal text." 329 | optional = false 330 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 331 | groups = ["test"] 332 | markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" 333 | files = [ 334 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 335 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 336 | ] 337 | 338 | [[package]] 339 | name = "configupdater" 340 | version = "3.2" 341 | description = "Parser like ConfigParser but for updating configuration files" 342 | optional = false 343 | python-versions = ">=3.6" 344 | groups = ["test"] 345 | files = [ 346 | {file = "ConfigUpdater-3.2-py2.py3-none-any.whl", hash = "sha256:0f65a041627d7693840b4dd743581db4c441c97195298a29d075f91b79539df2"}, 347 | {file = "ConfigUpdater-3.2.tar.gz", hash = "sha256:9fdac53831c1b062929bf398b649b87ca30e7f1a735f3fbf482072804106306b"}, 348 | ] 349 | 350 | [package.extras] 351 | testing = ["flake8", "pytest", "pytest-cov", "pytest-randomly", "pytest-xdist", "sphinx"] 352 | 353 | [[package]] 354 | name = "coverage" 355 | version = "7.6.1" 356 | description = "Code coverage measurement for Python" 357 | optional = false 358 | python-versions = ">=3.8" 359 | groups = ["test"] 360 | files = [ 361 | {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, 362 | {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, 363 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, 364 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, 365 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, 366 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, 367 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, 368 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, 369 | {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, 370 | {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, 371 | {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, 372 | {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, 373 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, 374 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, 375 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, 376 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, 377 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, 378 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, 379 | {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, 380 | {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, 381 | {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, 382 | {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, 383 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, 384 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, 385 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, 386 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, 387 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, 388 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, 389 | {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, 390 | {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, 391 | {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, 392 | {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, 393 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, 394 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, 395 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, 396 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, 397 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, 398 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, 399 | {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, 400 | {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, 401 | {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, 402 | {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, 403 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, 404 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, 405 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, 406 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, 407 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, 408 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, 409 | {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, 410 | {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, 411 | {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, 412 | {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, 413 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, 414 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, 415 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, 416 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, 417 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, 418 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, 419 | {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, 420 | {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, 421 | {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, 422 | {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, 423 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, 424 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, 425 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, 426 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, 427 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, 428 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, 429 | {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, 430 | {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, 431 | {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, 432 | {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, 433 | ] 434 | 435 | [package.dependencies] 436 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 437 | 438 | [package.extras] 439 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 440 | 441 | [[package]] 442 | name = "cryptography" 443 | version = "43.0.0" 444 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 445 | optional = false 446 | python-versions = ">=3.7" 447 | groups = ["test"] 448 | files = [ 449 | {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, 450 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, 451 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, 452 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, 453 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, 454 | {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, 455 | {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, 456 | {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, 457 | {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, 458 | {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, 459 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, 460 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, 461 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, 462 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, 463 | {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, 464 | {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, 465 | {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, 466 | {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, 467 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, 468 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, 469 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, 470 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, 471 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, 472 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, 473 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, 474 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, 475 | {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, 476 | ] 477 | 478 | [package.dependencies] 479 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 480 | 481 | [package.extras] 482 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 483 | docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] 484 | nox = ["nox"] 485 | pep8test = ["check-sdist", "click", "mypy", "ruff"] 486 | sdist = ["build"] 487 | ssh = ["bcrypt (>=3.1.5)"] 488 | test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 489 | test-randomorder = ["pytest-randomly"] 490 | 491 | [[package]] 492 | name = "darglint" 493 | version = "1.8.1" 494 | description = "A utility for ensuring Google-style docstrings stay up to date with the source code." 495 | optional = false 496 | python-versions = ">=3.6,<4.0" 497 | groups = ["test"] 498 | files = [ 499 | {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, 500 | {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, 501 | ] 502 | 503 | [[package]] 504 | name = "dictdiffer" 505 | version = "0.9.0" 506 | description = "Dictdiffer is a library that helps you to diff and patch dictionaries." 507 | optional = false 508 | python-versions = "*" 509 | groups = ["test"] 510 | files = [ 511 | {file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"}, 512 | {file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"}, 513 | ] 514 | 515 | [package.extras] 516 | all = ["Sphinx (>=3)", "check-manifest (>=0.42)", "mock (>=1.3.0)", "numpy (>=1.13.0) ; python_version < \"3.7\"", "numpy (>=1.15.0) ; python_version < \"3.8\"", "numpy (>=1.18.0) ; python_version < \"3.9\"", "numpy (>=1.20.0) ; python_version >= \"3.9\"", "pytest (==5.4.3) ; python_version <= \"3.5\"", "pytest (>=6) ; python_version > \"3.5\"", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2) ; python_version <= \"3.5\"", "pytest-pycodestyle (>=2.2.0) ; python_version > \"3.5\"", "pytest-pydocstyle (>=2) ; python_version <= \"3.5\"", "pytest-pydocstyle (>=2.2.0) ; python_version > \"3.5\"", "sphinx (>=3)", "sphinx-rtd-theme (>=0.2)", "tox (>=3.7.0)"] 517 | docs = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)"] 518 | numpy = ["numpy (>=1.13.0) ; python_version < \"3.7\"", "numpy (>=1.15.0) ; python_version < \"3.8\"", "numpy (>=1.18.0) ; python_version < \"3.9\"", "numpy (>=1.20.0) ; python_version >= \"3.9\""] 519 | tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest (==5.4.3) ; python_version <= \"3.5\"", "pytest (>=6) ; python_version > \"3.5\"", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2) ; python_version <= \"3.5\"", "pytest-pycodestyle (>=2.2.0) ; python_version > \"3.5\"", "pytest-pydocstyle (>=2) ; python_version <= \"3.5\"", "pytest-pydocstyle (>=2.2.0) ; python_version > \"3.5\"", "sphinx (>=3)", "tox (>=3.7.0)"] 520 | 521 | [[package]] 522 | name = "docutils" 523 | version = "0.21.2" 524 | description = "Docutils -- Python Documentation Utilities" 525 | optional = false 526 | python-versions = ">=3.9" 527 | groups = ["test"] 528 | files = [ 529 | {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, 530 | {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, 531 | ] 532 | 533 | [[package]] 534 | name = "dpath" 535 | version = "2.2.0" 536 | description = "Filesystem-like pathing and searching for dictionaries" 537 | optional = false 538 | python-versions = ">=3.7" 539 | groups = ["test"] 540 | files = [ 541 | {file = "dpath-2.2.0-py3-none-any.whl", hash = "sha256:b330a375ded0a0d2ed404440f6c6a715deae5313af40bbb01c8a41d891900576"}, 542 | {file = "dpath-2.2.0.tar.gz", hash = "sha256:34f7e630dc55ea3f219e555726f5da4b4b25f2200319c8e6902c394258dd6a3e"}, 543 | ] 544 | 545 | [[package]] 546 | name = "eradicate" 547 | version = "2.3.0" 548 | description = "Removes commented-out code." 549 | optional = false 550 | python-versions = "*" 551 | groups = ["test"] 552 | files = [ 553 | {file = "eradicate-2.3.0-py3-none-any.whl", hash = "sha256:2b29b3dd27171f209e4ddd8204b70c02f0682ae95eecb353f10e8d72b149c63e"}, 554 | {file = "eradicate-2.3.0.tar.gz", hash = "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37"}, 555 | ] 556 | 557 | [[package]] 558 | name = "exceptiongroup" 559 | version = "1.2.2" 560 | description = "Backport of PEP 654 (exception groups)" 561 | optional = false 562 | python-versions = ">=3.7" 563 | groups = ["test"] 564 | markers = "python_version < \"3.11\"" 565 | files = [ 566 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 567 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 568 | ] 569 | 570 | [package.extras] 571 | test = ["pytest (>=6)"] 572 | 573 | [[package]] 574 | name = "flake8" 575 | version = "7.1.1" 576 | description = "the modular source code checker: pep8 pyflakes and co" 577 | optional = false 578 | python-versions = ">=3.8.1" 579 | groups = ["test"] 580 | files = [ 581 | {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, 582 | {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, 583 | ] 584 | 585 | [package.dependencies] 586 | mccabe = ">=0.7.0,<0.8.0" 587 | pycodestyle = ">=2.12.0,<2.13.0" 588 | pyflakes = ">=3.2.0,<3.3.0" 589 | 590 | [[package]] 591 | name = "flake8-bandit" 592 | version = "4.1.1" 593 | description = "Automated security testing with bandit and flake8." 594 | optional = false 595 | python-versions = ">=3.6" 596 | groups = ["test"] 597 | files = [ 598 | {file = "flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d"}, 599 | {file = "flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e"}, 600 | ] 601 | 602 | [package.dependencies] 603 | bandit = ">=1.7.3" 604 | flake8 = ">=5.0.0" 605 | 606 | [[package]] 607 | name = "flake8-broken-line" 608 | version = "1.0.0" 609 | description = "Flake8 plugin to forbid backslashes for line breaks" 610 | optional = false 611 | python-versions = ">=3.8,<4.0" 612 | groups = ["test"] 613 | files = [ 614 | {file = "flake8_broken_line-1.0.0-py3-none-any.whl", hash = "sha256:96c964336024a5030dc536a9f6fb02aa679e2d2a6b35b80a558b5136c35832a9"}, 615 | {file = "flake8_broken_line-1.0.0.tar.gz", hash = "sha256:e2c6a17f8d9a129e99c1320fce89b33843e2963871025c4c2bb7b8b8d8732a85"}, 616 | ] 617 | 618 | [package.dependencies] 619 | flake8 = ">5" 620 | 621 | [[package]] 622 | name = "flake8-bugbear" 623 | version = "24.4.26" 624 | description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." 625 | optional = false 626 | python-versions = ">=3.8.1" 627 | groups = ["test"] 628 | files = [ 629 | {file = "flake8_bugbear-24.4.26-py3-none-any.whl", hash = "sha256:cb430dd86bc821d79ccc0b030789a9c87a47a369667f12ba06e80f11305e8258"}, 630 | {file = "flake8_bugbear-24.4.26.tar.gz", hash = "sha256:ff8d4ba5719019ebf98e754624c30c05cef0dadcf18a65d91c7567300e52a130"}, 631 | ] 632 | 633 | [package.dependencies] 634 | attrs = ">=19.2.0" 635 | flake8 = ">=6.0.0" 636 | 637 | [package.extras] 638 | dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"] 639 | 640 | [[package]] 641 | name = "flake8-commas" 642 | version = "2.1.0" 643 | description = "Flake8 lint for trailing commas." 644 | optional = false 645 | python-versions = "*" 646 | groups = ["test"] 647 | files = [ 648 | {file = "flake8-commas-2.1.0.tar.gz", hash = "sha256:940441ab8ee544df564ae3b3f49f20462d75d5c7cac2463e0b27436e2050f263"}, 649 | {file = "flake8_commas-2.1.0-py2.py3-none-any.whl", hash = "sha256:ebb96c31e01d0ef1d0685a21f3f0e2f8153a0381430e748bf0bbbb5d5b453d54"}, 650 | ] 651 | 652 | [package.dependencies] 653 | flake8 = ">=2" 654 | 655 | [[package]] 656 | name = "flake8-comprehensions" 657 | version = "3.15.0" 658 | description = "A flake8 plugin to help you write better list/set/dict comprehensions." 659 | optional = false 660 | python-versions = ">=3.8" 661 | groups = ["test"] 662 | files = [ 663 | {file = "flake8_comprehensions-3.15.0-py3-none-any.whl", hash = "sha256:b7e027bbb52be2ceb779ee12484cdeef52b0ad3c1fcb8846292bdb86d3034681"}, 664 | {file = "flake8_comprehensions-3.15.0.tar.gz", hash = "sha256:923c22603e0310376a6b55b03efebdc09753c69f2d977755cba8bb73458a5d4d"}, 665 | ] 666 | 667 | [package.dependencies] 668 | flake8 = ">=3,<3.2 || >3.2" 669 | 670 | [[package]] 671 | name = "flake8-debugger" 672 | version = "4.1.2" 673 | description = "ipdb/pdb statement checker plugin for flake8" 674 | optional = false 675 | python-versions = ">=3.7" 676 | groups = ["test"] 677 | files = [ 678 | {file = "flake8-debugger-4.1.2.tar.gz", hash = "sha256:52b002560941e36d9bf806fca2523dc7fb8560a295d5f1a6e15ac2ded7a73840"}, 679 | {file = "flake8_debugger-4.1.2-py3-none-any.whl", hash = "sha256:0a5e55aeddcc81da631ad9c8c366e7318998f83ff00985a49e6b3ecf61e571bf"}, 680 | ] 681 | 682 | [package.dependencies] 683 | flake8 = ">=3.0" 684 | pycodestyle = "*" 685 | 686 | [[package]] 687 | name = "flake8-docstrings" 688 | version = "1.7.0" 689 | description = "Extension for flake8 which uses pydocstyle to check docstrings" 690 | optional = false 691 | python-versions = ">=3.7" 692 | groups = ["test"] 693 | files = [ 694 | {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, 695 | {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, 696 | ] 697 | 698 | [package.dependencies] 699 | flake8 = ">=3" 700 | pydocstyle = ">=2.1" 701 | 702 | [[package]] 703 | name = "flake8-eradicate" 704 | version = "1.5.0" 705 | description = "Flake8 plugin to find commented out code" 706 | optional = false 707 | python-versions = ">=3.8,<4.0" 708 | groups = ["test"] 709 | files = [ 710 | {file = "flake8_eradicate-1.5.0-py3-none-any.whl", hash = "sha256:18acc922ad7de623f5247c7d5595da068525ec5437dd53b22ec2259b96ce9d22"}, 711 | {file = "flake8_eradicate-1.5.0.tar.gz", hash = "sha256:aee636cb9ecb5594a7cd92d67ad73eb69909e5cc7bd81710cf9d00970f3983a6"}, 712 | ] 713 | 714 | [package.dependencies] 715 | attrs = "*" 716 | eradicate = ">=2.0,<3.0" 717 | flake8 = ">5" 718 | 719 | [[package]] 720 | name = "flake8-isort" 721 | version = "6.1.1" 722 | description = "flake8 plugin that integrates isort" 723 | optional = false 724 | python-versions = ">=3.8" 725 | groups = ["test"] 726 | files = [ 727 | {file = "flake8_isort-6.1.1-py3-none-any.whl", hash = "sha256:0fec4dc3a15aefbdbe4012e51d5531a2eb5fa8b981cdfbc882296a59b54ede12"}, 728 | {file = "flake8_isort-6.1.1.tar.gz", hash = "sha256:c1f82f3cf06a80c13e1d09bfae460e9666255d5c780b859f19f8318d420370b3"}, 729 | ] 730 | 731 | [package.dependencies] 732 | flake8 = "*" 733 | isort = ">=5.0.0,<6" 734 | 735 | [package.extras] 736 | test = ["pytest"] 737 | 738 | [[package]] 739 | name = "flake8-plugin-utils" 740 | version = "1.3.3" 741 | description = "The package provides base classes and utils for flake8 plugin writing" 742 | optional = false 743 | python-versions = ">=3.6,<4.0" 744 | groups = ["test"] 745 | files = [ 746 | {file = "flake8-plugin-utils-1.3.3.tar.gz", hash = "sha256:39f6f338d038b301c6fd344b06f2e81e382b68fa03c0560dff0d9b1791a11a2c"}, 747 | {file = "flake8_plugin_utils-1.3.3-py3-none-any.whl", hash = "sha256:e4848c57d9d50f19100c2d75fa794b72df068666a9041b4b0409be923356a3ed"}, 748 | ] 749 | 750 | [[package]] 751 | name = "flake8-pytest-style" 752 | version = "2.1.0" 753 | description = "A flake8 plugin checking common style issues or inconsistencies with pytest-based tests." 754 | optional = false 755 | python-versions = ">=3.9" 756 | groups = ["test"] 757 | files = [ 758 | {file = "flake8_pytest_style-2.1.0-py3-none-any.whl", hash = "sha256:a0d6dddcd533bfc13f19b8445907be0330c5e6ccf7090bcd9d5fa5a0b1b65e71"}, 759 | {file = "flake8_pytest_style-2.1.0.tar.gz", hash = "sha256:fee6befdb5915d600ef24e38d48a077d0dcffb032945ae0169486e7ff8a1079a"}, 760 | ] 761 | 762 | [package.dependencies] 763 | flake8-plugin-utils = ">=1.3.2,<2.0.0" 764 | 765 | [[package]] 766 | name = "flake8-quotes" 767 | version = "3.4.0" 768 | description = "Flake8 lint for quotes." 769 | optional = false 770 | python-versions = "*" 771 | groups = ["test"] 772 | files = [ 773 | {file = "flake8-quotes-3.4.0.tar.gz", hash = "sha256:aad8492fb710a2d3eabe68c5f86a1428de650c8484127e14c43d0504ba30276c"}, 774 | ] 775 | 776 | [package.dependencies] 777 | flake8 = "*" 778 | setuptools = "*" 779 | 780 | [[package]] 781 | name = "flake8-rst-docstrings" 782 | version = "0.3.0" 783 | description = "Python docstring reStructuredText (RST) validator for flake8" 784 | optional = false 785 | python-versions = ">=3.7" 786 | groups = ["test"] 787 | files = [ 788 | {file = "flake8-rst-docstrings-0.3.0.tar.gz", hash = "sha256:d1ce22b4bd37b73cd86b8d980e946ef198cfcc18ed82fedb674ceaa2f8d1afa4"}, 789 | {file = "flake8_rst_docstrings-0.3.0-py3-none-any.whl", hash = "sha256:f8c3c6892ff402292651c31983a38da082480ad3ba253743de52989bdc84ca1c"}, 790 | ] 791 | 792 | [package.dependencies] 793 | flake8 = ">=3" 794 | pygments = "*" 795 | restructuredtext-lint = "*" 796 | 797 | [package.extras] 798 | develop = ["build", "twine"] 799 | 800 | [[package]] 801 | name = "flake8-string-format" 802 | version = "0.3.0" 803 | description = "string format checker, plugin for flake8" 804 | optional = false 805 | python-versions = "*" 806 | groups = ["test"] 807 | files = [ 808 | {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, 809 | {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, 810 | ] 811 | 812 | [package.dependencies] 813 | flake8 = "*" 814 | 815 | [[package]] 816 | name = "flatten-dict" 817 | version = "0.4.2" 818 | description = "A flexible utility for flattening and unflattening dict-like objects in Python." 819 | optional = false 820 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 821 | groups = ["test"] 822 | files = [ 823 | {file = "flatten-dict-0.4.2.tar.gz", hash = "sha256:506a96b6e6f805b81ae46a0f9f31290beb5fa79ded9d80dbe1b7fa236ab43076"}, 824 | {file = "flatten_dict-0.4.2-py2.py3-none-any.whl", hash = "sha256:7e245b20c4c718981212210eec4284a330c9f713e632e98765560e05421e48ad"}, 825 | ] 826 | 827 | [package.dependencies] 828 | six = ">=1.12,<2.0" 829 | 830 | [[package]] 831 | name = "freezegun" 832 | version = "1.5.1" 833 | description = "Let your Python tests travel through time" 834 | optional = false 835 | python-versions = ">=3.7" 836 | groups = ["test"] 837 | files = [ 838 | {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, 839 | {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, 840 | ] 841 | 842 | [package.dependencies] 843 | python-dateutil = ">=2.7" 844 | 845 | [[package]] 846 | name = "furl" 847 | version = "2.1.3" 848 | description = "URL manipulation made simple." 849 | optional = false 850 | python-versions = "*" 851 | groups = ["test"] 852 | files = [ 853 | {file = "furl-2.1.3-py2.py3-none-any.whl", hash = "sha256:9ab425062c4217f9802508e45feb4a83e54324273ac4b202f1850363309666c0"}, 854 | {file = "furl-2.1.3.tar.gz", hash = "sha256:5a6188fe2666c484a12159c18be97a1977a71d632ef5bb867ef15f54af39cc4e"}, 855 | ] 856 | 857 | [package.dependencies] 858 | orderedmultidict = ">=1.0.1" 859 | six = ">=1.8.0" 860 | 861 | [[package]] 862 | name = "gitignore-parser" 863 | version = "0.1.11" 864 | description = "A spec-compliant gitignore parser for Python 3.5+" 865 | optional = false 866 | python-versions = "*" 867 | groups = ["test"] 868 | files = [ 869 | {file = "gitignore_parser-0.1.11.tar.gz", hash = "sha256:fa10fde48b44888eeefac096f53bcdad9b87a4ffd7db788558dbdf71ff3bc9db"}, 870 | ] 871 | 872 | [[package]] 873 | name = "identify" 874 | version = "2.6.0" 875 | description = "File identification library for Python" 876 | optional = false 877 | python-versions = ">=3.8" 878 | groups = ["test"] 879 | files = [ 880 | {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, 881 | {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, 882 | ] 883 | 884 | [package.extras] 885 | license = ["ukkonen"] 886 | 887 | [[package]] 888 | name = "idna" 889 | version = "3.7" 890 | description = "Internationalized Domain Names in Applications (IDNA)" 891 | optional = false 892 | python-versions = ">=3.5" 893 | groups = ["test"] 894 | files = [ 895 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 896 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 897 | ] 898 | 899 | [[package]] 900 | name = "importlib-metadata" 901 | version = "8.2.0" 902 | description = "Read metadata from Python packages" 903 | optional = false 904 | python-versions = ">=3.8" 905 | groups = ["test"] 906 | markers = "python_version < \"3.10\"" 907 | files = [ 908 | {file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"}, 909 | {file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"}, 910 | ] 911 | 912 | [package.dependencies] 913 | zipp = ">=0.5" 914 | 915 | [package.extras] 916 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 917 | perf = ["ipython"] 918 | test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] 919 | 920 | [[package]] 921 | name = "iniconfig" 922 | version = "2.0.0" 923 | description = "brain-dead simple config-ini parsing" 924 | optional = false 925 | python-versions = ">=3.7" 926 | groups = ["test"] 927 | files = [ 928 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 929 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 930 | ] 931 | 932 | [[package]] 933 | name = "isort" 934 | version = "5.13.2" 935 | description = "A Python utility / library to sort Python imports." 936 | optional = false 937 | python-versions = ">=3.8.0" 938 | groups = ["test"] 939 | files = [ 940 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 941 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 942 | ] 943 | 944 | [package.extras] 945 | colors = ["colorama (>=0.4.6)"] 946 | 947 | [[package]] 948 | name = "jmespath" 949 | version = "1.0.1" 950 | description = "JSON Matching Expressions" 951 | optional = false 952 | python-versions = ">=3.7" 953 | groups = ["test"] 954 | files = [ 955 | {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, 956 | {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, 957 | ] 958 | 959 | [[package]] 960 | name = "loguru" 961 | version = "0.7.2" 962 | description = "Python logging made (stupidly) simple" 963 | optional = false 964 | python-versions = ">=3.5" 965 | groups = ["test"] 966 | files = [ 967 | {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, 968 | {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, 969 | ] 970 | 971 | [package.dependencies] 972 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 973 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 974 | 975 | [package.extras] 976 | dev = ["Sphinx (==7.2.5) ; python_version >= \"3.9\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.2.2) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "mypy (==v1.5.1) ; python_version >= \"3.8\"", "pre-commit (==3.4.0) ; python_version >= \"3.8\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==7.4.0) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==4.1.0) ; python_version >= \"3.8\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.0.0) ; python_version >= \"3.8\"", "sphinx-autobuild (==2021.3.14) ; python_version >= \"3.9\"", "sphinx-rtd-theme (==1.3.0) ; python_version >= \"3.9\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.11.0) ; python_version >= \"3.8\""] 977 | 978 | [[package]] 979 | name = "markdown-it-py" 980 | version = "3.0.0" 981 | description = "Python port of markdown-it. Markdown parsing, done right!" 982 | optional = false 983 | python-versions = ">=3.8" 984 | groups = ["test"] 985 | files = [ 986 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 987 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 988 | ] 989 | 990 | [package.dependencies] 991 | mdurl = ">=0.1,<1.0" 992 | 993 | [package.extras] 994 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 995 | code-style = ["pre-commit (>=3.0,<4.0)"] 996 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 997 | linkify = ["linkify-it-py (>=1,<3)"] 998 | plugins = ["mdit-py-plugins"] 999 | profiling = ["gprof2dot"] 1000 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 1001 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 1002 | 1003 | [[package]] 1004 | name = "marshmallow" 1005 | version = "3.21.3" 1006 | description = "A lightweight library for converting complex datatypes to and from native Python datatypes." 1007 | optional = false 1008 | python-versions = ">=3.8" 1009 | groups = ["test"] 1010 | files = [ 1011 | {file = "marshmallow-3.21.3-py3-none-any.whl", hash = "sha256:86ce7fb914aa865001a4b2092c4c2872d13bc347f3d42673272cabfdbad386f1"}, 1012 | {file = "marshmallow-3.21.3.tar.gz", hash = "sha256:4f57c5e050a54d66361e826f94fba213eb10b67b2fdb02c3e0343ce207ba1662"}, 1013 | ] 1014 | 1015 | [package.dependencies] 1016 | packaging = ">=17.0" 1017 | 1018 | [package.extras] 1019 | dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] 1020 | docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.3.7)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] 1021 | tests = ["pytest", "pytz", "simplejson"] 1022 | 1023 | [[package]] 1024 | name = "marshmallow-polyfield" 1025 | version = "5.11" 1026 | description = "An unofficial extension to Marshmallow to allow for polymorphic fields" 1027 | optional = false 1028 | python-versions = ">=3.5" 1029 | groups = ["test"] 1030 | files = [ 1031 | {file = "marshmallow-polyfield-5.11.tar.gz", hash = "sha256:8075a9cc490da4af58b902b4a40a99882dd031adb7aaa96abd147a4fcd53415f"}, 1032 | ] 1033 | 1034 | [package.dependencies] 1035 | marshmallow = ">=3.0.0b10" 1036 | 1037 | [[package]] 1038 | name = "mccabe" 1039 | version = "0.7.0" 1040 | description = "McCabe checker, plugin for flake8" 1041 | optional = false 1042 | python-versions = ">=3.6" 1043 | groups = ["test"] 1044 | files = [ 1045 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 1046 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "mdurl" 1051 | version = "0.1.2" 1052 | description = "Markdown URL utilities" 1053 | optional = false 1054 | python-versions = ">=3.7" 1055 | groups = ["test"] 1056 | files = [ 1057 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 1058 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 1059 | ] 1060 | 1061 | [[package]] 1062 | name = "more-itertools" 1063 | version = "10.3.0" 1064 | description = "More routines for operating on iterables, beyond itertools" 1065 | optional = false 1066 | python-versions = ">=3.8" 1067 | groups = ["test"] 1068 | files = [ 1069 | {file = "more-itertools-10.3.0.tar.gz", hash = "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463"}, 1070 | {file = "more_itertools-10.3.0-py3-none-any.whl", hash = "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320"}, 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "mypy" 1075 | version = "1.16.0" 1076 | description = "Optional static typing for Python" 1077 | optional = false 1078 | python-versions = ">=3.9" 1079 | groups = ["test"] 1080 | files = [ 1081 | {file = "mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c"}, 1082 | {file = "mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571"}, 1083 | {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491"}, 1084 | {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777"}, 1085 | {file = "mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b"}, 1086 | {file = "mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93"}, 1087 | {file = "mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab"}, 1088 | {file = "mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2"}, 1089 | {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff"}, 1090 | {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666"}, 1091 | {file = "mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c"}, 1092 | {file = "mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b"}, 1093 | {file = "mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13"}, 1094 | {file = "mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090"}, 1095 | {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1"}, 1096 | {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8"}, 1097 | {file = "mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730"}, 1098 | {file = "mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec"}, 1099 | {file = "mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b"}, 1100 | {file = "mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0"}, 1101 | {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b"}, 1102 | {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d"}, 1103 | {file = "mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52"}, 1104 | {file = "mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb"}, 1105 | {file = "mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3"}, 1106 | {file = "mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92"}, 1107 | {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436"}, 1108 | {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2"}, 1109 | {file = "mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20"}, 1110 | {file = "mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21"}, 1111 | {file = "mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031"}, 1112 | {file = "mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab"}, 1113 | ] 1114 | 1115 | [package.dependencies] 1116 | mypy_extensions = ">=1.0.0" 1117 | pathspec = ">=0.9.0" 1118 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 1119 | typing_extensions = ">=4.6.0" 1120 | 1121 | [package.extras] 1122 | dmypy = ["psutil (>=4.0)"] 1123 | faster-cache = ["orjson"] 1124 | install-types = ["pip"] 1125 | mypyc = ["setuptools (>=50)"] 1126 | reports = ["lxml"] 1127 | 1128 | [[package]] 1129 | name = "mypy-extensions" 1130 | version = "1.0.0" 1131 | description = "Type system extensions for programs checked with the mypy type checker." 1132 | optional = false 1133 | python-versions = ">=3.5" 1134 | groups = ["test"] 1135 | files = [ 1136 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 1137 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 1138 | ] 1139 | 1140 | [[package]] 1141 | name = "nitpick" 1142 | version = "0.35.0" 1143 | description = "Enforce the same settings across multiple language-independent projects" 1144 | optional = false 1145 | python-versions = ">=3.8,<4.0" 1146 | groups = ["test"] 1147 | files = [ 1148 | {file = "nitpick-0.35.0-py3-none-any.whl", hash = "sha256:9911d32c2d488b41914aa1a6d230531fa92bbe3d6610e4a468a5cb5e30fab907"}, 1149 | {file = "nitpick-0.35.0.tar.gz", hash = "sha256:098167a4c65655aca52c0ea3876b1e71cf634a27d0e17b971bce9bfcc1f3febe"}, 1150 | ] 1151 | 1152 | [package.dependencies] 1153 | attrs = ">=20.1.0" 1154 | autorepr = "*" 1155 | click = "*" 1156 | ConfigUpdater = "*" 1157 | dictdiffer = "*" 1158 | dpath = "*" 1159 | flake8 = ">=3.0.0" 1160 | flatten-dict = "*" 1161 | furl = "*" 1162 | gitignore_parser = "*" 1163 | identify = "*" 1164 | jmespath = "*" 1165 | loguru = "*" 1166 | marshmallow = ">=3.0.0b10" 1167 | marshmallow-polyfield = ">=5.10,<6.0" 1168 | more-itertools = "*" 1169 | packaging = "*" 1170 | pluggy = "*" 1171 | python-slugify = "*" 1172 | requests = "*" 1173 | requests-cache = ">=1.0.0" 1174 | "ruamel.yaml" = "*" 1175 | sortedcontainers = "*" 1176 | StrEnum = "*" 1177 | toml = "*" 1178 | tomlkit = ">=0.8.0" 1179 | 1180 | [package.extras] 1181 | doc = ["sphinx", "sphinx-gitref", "sphinx_rtd_theme", "sphobjinv"] 1182 | lint = ["pylint"] 1183 | test = ["freezegun", "pytest", "pytest-cov", "pytest-datadir", "pytest-socket", "pytest-testmon", "pytest-watch", "responses", "testfixtures"] 1184 | 1185 | [[package]] 1186 | name = "orderedmultidict" 1187 | version = "1.0.1" 1188 | description = "Ordered Multivalue Dictionary" 1189 | optional = false 1190 | python-versions = "*" 1191 | groups = ["test"] 1192 | files = [ 1193 | {file = "orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"}, 1194 | {file = "orderedmultidict-1.0.1.tar.gz", hash = "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad"}, 1195 | ] 1196 | 1197 | [package.dependencies] 1198 | six = ">=1.8.0" 1199 | 1200 | [[package]] 1201 | name = "packaging" 1202 | version = "24.1" 1203 | description = "Core utilities for Python packages" 1204 | optional = false 1205 | python-versions = ">=3.8" 1206 | groups = ["test"] 1207 | files = [ 1208 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 1209 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "pathspec" 1214 | version = "0.12.1" 1215 | description = "Utility library for gitignore style pattern matching of file paths." 1216 | optional = false 1217 | python-versions = ">=3.8" 1218 | groups = ["test"] 1219 | files = [ 1220 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 1221 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 1222 | ] 1223 | 1224 | [[package]] 1225 | name = "pbr" 1226 | version = "6.0.0" 1227 | description = "Python Build Reasonableness" 1228 | optional = false 1229 | python-versions = ">=2.6" 1230 | groups = ["test"] 1231 | files = [ 1232 | {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, 1233 | {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, 1234 | ] 1235 | 1236 | [[package]] 1237 | name = "pep8-naming" 1238 | version = "0.13.3" 1239 | description = "Check PEP-8 naming conventions, plugin for flake8" 1240 | optional = false 1241 | python-versions = ">=3.7" 1242 | groups = ["test"] 1243 | files = [ 1244 | {file = "pep8-naming-0.13.3.tar.gz", hash = "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971"}, 1245 | {file = "pep8_naming-0.13.3-py3-none-any.whl", hash = "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80"}, 1246 | ] 1247 | 1248 | [package.dependencies] 1249 | flake8 = ">=5.0.0" 1250 | 1251 | [[package]] 1252 | name = "platformdirs" 1253 | version = "4.2.2" 1254 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 1255 | optional = false 1256 | python-versions = ">=3.8" 1257 | groups = ["test"] 1258 | files = [ 1259 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 1260 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 1261 | ] 1262 | 1263 | [package.extras] 1264 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 1265 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 1266 | type = ["mypy (>=1.8)"] 1267 | 1268 | [[package]] 1269 | name = "pluggy" 1270 | version = "1.5.0" 1271 | description = "plugin and hook calling mechanisms for python" 1272 | optional = false 1273 | python-versions = ">=3.8" 1274 | groups = ["test"] 1275 | files = [ 1276 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 1277 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 1278 | ] 1279 | 1280 | [package.extras] 1281 | dev = ["pre-commit", "tox"] 1282 | testing = ["pytest", "pytest-benchmark"] 1283 | 1284 | [[package]] 1285 | name = "pycodestyle" 1286 | version = "2.12.1" 1287 | description = "Python style guide checker" 1288 | optional = false 1289 | python-versions = ">=3.8" 1290 | groups = ["test"] 1291 | files = [ 1292 | {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, 1293 | {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, 1294 | ] 1295 | 1296 | [[package]] 1297 | name = "pycparser" 1298 | version = "2.22" 1299 | description = "C parser in Python" 1300 | optional = false 1301 | python-versions = ">=3.8" 1302 | groups = ["test"] 1303 | markers = "platform_python_implementation != \"PyPy\"" 1304 | files = [ 1305 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 1306 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 1307 | ] 1308 | 1309 | [[package]] 1310 | name = "pydocstyle" 1311 | version = "6.3.0" 1312 | description = "Python docstring style checker" 1313 | optional = false 1314 | python-versions = ">=3.6" 1315 | groups = ["test"] 1316 | files = [ 1317 | {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, 1318 | {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, 1319 | ] 1320 | 1321 | [package.dependencies] 1322 | snowballstemmer = ">=2.2.0" 1323 | 1324 | [package.extras] 1325 | toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""] 1326 | 1327 | [[package]] 1328 | name = "pyflakes" 1329 | version = "3.2.0" 1330 | description = "passive checker of Python programs" 1331 | optional = false 1332 | python-versions = ">=3.8" 1333 | groups = ["test"] 1334 | files = [ 1335 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 1336 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 1337 | ] 1338 | 1339 | [[package]] 1340 | name = "pygments" 1341 | version = "2.18.0" 1342 | description = "Pygments is a syntax highlighting package written in Python." 1343 | optional = false 1344 | python-versions = ">=3.8" 1345 | groups = ["test"] 1346 | files = [ 1347 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 1348 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 1349 | ] 1350 | 1351 | [package.extras] 1352 | windows-terminal = ["colorama (>=0.4.6)"] 1353 | 1354 | [[package]] 1355 | name = "pytest" 1356 | version = "8.4.0" 1357 | description = "pytest: simple powerful testing with Python" 1358 | optional = false 1359 | python-versions = ">=3.9" 1360 | groups = ["test"] 1361 | files = [ 1362 | {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"}, 1363 | {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"}, 1364 | ] 1365 | 1366 | [package.dependencies] 1367 | colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 1368 | exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} 1369 | iniconfig = ">=1" 1370 | packaging = ">=20" 1371 | pluggy = ">=1.5,<2" 1372 | pygments = ">=2.7.2" 1373 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 1374 | 1375 | [package.extras] 1376 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 1377 | 1378 | [[package]] 1379 | name = "pytest-asyncio" 1380 | version = "0.26.0" 1381 | description = "Pytest support for asyncio" 1382 | optional = false 1383 | python-versions = ">=3.9" 1384 | groups = ["test"] 1385 | files = [ 1386 | {file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"}, 1387 | {file = "pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f"}, 1388 | ] 1389 | 1390 | [package.dependencies] 1391 | pytest = ">=8.2,<9" 1392 | typing-extensions = {version = ">=4.12", markers = "python_version < \"3.10\""} 1393 | 1394 | [package.extras] 1395 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] 1396 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 1397 | 1398 | [[package]] 1399 | name = "pytest-cov" 1400 | version = "6.1.1" 1401 | description = "Pytest plugin for measuring coverage." 1402 | optional = false 1403 | python-versions = ">=3.9" 1404 | groups = ["test"] 1405 | files = [ 1406 | {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, 1407 | {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, 1408 | ] 1409 | 1410 | [package.dependencies] 1411 | coverage = {version = ">=7.5", extras = ["toml"]} 1412 | pytest = ">=4.6" 1413 | 1414 | [package.extras] 1415 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 1416 | 1417 | [[package]] 1418 | name = "pytest-freezegun" 1419 | version = "0.4.2" 1420 | description = "Wrap tests with fixtures in freeze_time" 1421 | optional = false 1422 | python-versions = "*" 1423 | groups = ["test"] 1424 | files = [ 1425 | {file = "pytest-freezegun-0.4.2.zip", hash = "sha256:19c82d5633751bf3ec92caa481fb5cffaac1787bd485f0df6436fd6242176949"}, 1426 | {file = "pytest_freezegun-0.4.2-py2.py3-none-any.whl", hash = "sha256:5318a6bfb8ba4b709c8471c94d0033113877b3ee02da5bfcd917c1889cde99a7"}, 1427 | ] 1428 | 1429 | [package.dependencies] 1430 | freezegun = ">0.3" 1431 | pytest = ">=3.0.0" 1432 | 1433 | [[package]] 1434 | name = "pytest-randomly" 1435 | version = "3.16.0" 1436 | description = "Pytest plugin to randomly order tests and control random.seed." 1437 | optional = false 1438 | python-versions = ">=3.9" 1439 | groups = ["test"] 1440 | files = [ 1441 | {file = "pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6"}, 1442 | {file = "pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26"}, 1443 | ] 1444 | 1445 | [package.dependencies] 1446 | importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} 1447 | pytest = "*" 1448 | 1449 | [[package]] 1450 | name = "pytest-repeat" 1451 | version = "0.9.4" 1452 | description = "pytest plugin for repeating tests" 1453 | optional = false 1454 | python-versions = ">=3.9" 1455 | groups = ["test"] 1456 | files = [ 1457 | {file = "pytest_repeat-0.9.4-py3-none-any.whl", hash = "sha256:c1738b4e412a6f3b3b9e0b8b29fcd7a423e50f87381ad9307ef6f5a8601139f3"}, 1458 | {file = "pytest_repeat-0.9.4.tar.gz", hash = "sha256:d92ac14dfaa6ffcfe6917e5d16f0c9bc82380c135b03c2a5f412d2637f224485"}, 1459 | ] 1460 | 1461 | [package.dependencies] 1462 | pytest = "*" 1463 | 1464 | [[package]] 1465 | name = "python-dateutil" 1466 | version = "2.9.0.post0" 1467 | description = "Extensions to the standard Python datetime module" 1468 | optional = false 1469 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 1470 | groups = ["test"] 1471 | files = [ 1472 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 1473 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 1474 | ] 1475 | 1476 | [package.dependencies] 1477 | six = ">=1.5" 1478 | 1479 | [[package]] 1480 | name = "python-slugify" 1481 | version = "8.0.4" 1482 | description = "A Python slugify application that also handles Unicode" 1483 | optional = false 1484 | python-versions = ">=3.7" 1485 | groups = ["test"] 1486 | files = [ 1487 | {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, 1488 | {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, 1489 | ] 1490 | 1491 | [package.dependencies] 1492 | text-unidecode = ">=1.3" 1493 | 1494 | [package.extras] 1495 | unidecode = ["Unidecode (>=1.1.1)"] 1496 | 1497 | [[package]] 1498 | name = "pyyaml" 1499 | version = "6.0.1" 1500 | description = "YAML parser and emitter for Python" 1501 | optional = false 1502 | python-versions = ">=3.6" 1503 | groups = ["test"] 1504 | files = [ 1505 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 1506 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 1507 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 1508 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 1509 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 1510 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 1511 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 1512 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 1513 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 1514 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 1515 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 1516 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 1517 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 1518 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 1519 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 1520 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 1521 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 1522 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 1523 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 1524 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 1525 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 1526 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 1527 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 1528 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 1529 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 1530 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 1531 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 1532 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 1533 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 1534 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 1535 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 1536 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 1537 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 1538 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 1539 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 1540 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 1541 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 1542 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 1543 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 1544 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 1545 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 1546 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 1547 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 1548 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 1549 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 1550 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 1551 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 1552 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 1553 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 1554 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 1555 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 1556 | ] 1557 | 1558 | [[package]] 1559 | name = "redis" 1560 | version = "6.2.0" 1561 | description = "Python client for Redis database and key-value store" 1562 | optional = true 1563 | python-versions = ">=3.9" 1564 | groups = ["main"] 1565 | markers = "extra == \"dev\" or extra == \"redis\"" 1566 | files = [ 1567 | {file = "redis-6.2.0-py3-none-any.whl", hash = "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e"}, 1568 | {file = "redis-6.2.0.tar.gz", hash = "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977"}, 1569 | ] 1570 | 1571 | [package.dependencies] 1572 | async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} 1573 | 1574 | [package.extras] 1575 | hiredis = ["hiredis (>=3.2.0)"] 1576 | jwt = ["pyjwt (>=2.9.0)"] 1577 | ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] 1578 | 1579 | [[package]] 1580 | name = "requests" 1581 | version = "2.32.3" 1582 | description = "Python HTTP for Humans." 1583 | optional = false 1584 | python-versions = ">=3.8" 1585 | groups = ["test"] 1586 | files = [ 1587 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 1588 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 1589 | ] 1590 | 1591 | [package.dependencies] 1592 | certifi = ">=2017.4.17" 1593 | charset-normalizer = ">=2,<4" 1594 | idna = ">=2.5,<4" 1595 | urllib3 = ">=1.21.1,<3" 1596 | 1597 | [package.extras] 1598 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 1599 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 1600 | 1601 | [[package]] 1602 | name = "requests-cache" 1603 | version = "1.2.1" 1604 | description = "A persistent cache for python requests" 1605 | optional = false 1606 | python-versions = ">=3.8" 1607 | groups = ["test"] 1608 | files = [ 1609 | {file = "requests_cache-1.2.1-py3-none-any.whl", hash = "sha256:1285151cddf5331067baa82598afe2d47c7495a1334bfe7a7d329b43e9fd3603"}, 1610 | {file = "requests_cache-1.2.1.tar.gz", hash = "sha256:68abc986fdc5b8d0911318fbb5f7c80eebcd4d01bfacc6685ecf8876052511d1"}, 1611 | ] 1612 | 1613 | [package.dependencies] 1614 | attrs = ">=21.2" 1615 | cattrs = ">=22.2" 1616 | platformdirs = ">=2.5" 1617 | requests = ">=2.22" 1618 | url-normalize = ">=1.4" 1619 | urllib3 = ">=1.25.5" 1620 | 1621 | [package.extras] 1622 | all = ["boto3 (>=1.15)", "botocore (>=1.18)", "itsdangerous (>=2.0)", "pymongo (>=3)", "pyyaml (>=6.0.1)", "redis (>=3)", "ujson (>=5.4)"] 1623 | bson = ["bson (>=0.5)"] 1624 | docs = ["furo (>=2023.3,<2024.0)", "linkify-it-py (>=2.0,<3.0)", "myst-parser (>=1.0,<2.0)", "sphinx (>=5.0.2,<6.0.0)", "sphinx-autodoc-typehints (>=1.19)", "sphinx-automodapi (>=0.14)", "sphinx-copybutton (>=0.5)", "sphinx-design (>=0.2)", "sphinx-notfound-page (>=0.8)", "sphinxcontrib-apidoc (>=0.3)", "sphinxext-opengraph (>=0.9)"] 1625 | dynamodb = ["boto3 (>=1.15)", "botocore (>=1.18)"] 1626 | json = ["ujson (>=5.4)"] 1627 | mongodb = ["pymongo (>=3)"] 1628 | redis = ["redis (>=3)"] 1629 | security = ["itsdangerous (>=2.0)"] 1630 | yaml = ["pyyaml (>=6.0.1)"] 1631 | 1632 | [[package]] 1633 | name = "restructuredtext-lint" 1634 | version = "1.4.0" 1635 | description = "reStructuredText linter" 1636 | optional = false 1637 | python-versions = "*" 1638 | groups = ["test"] 1639 | files = [ 1640 | {file = "restructuredtext_lint-1.4.0.tar.gz", hash = "sha256:1b235c0c922341ab6c530390892eb9e92f90b9b75046063e047cacfb0f050c45"}, 1641 | ] 1642 | 1643 | [package.dependencies] 1644 | docutils = ">=0.11,<1.0" 1645 | 1646 | [[package]] 1647 | name = "rich" 1648 | version = "13.7.1" 1649 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 1650 | optional = false 1651 | python-versions = ">=3.7.0" 1652 | groups = ["test"] 1653 | files = [ 1654 | {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, 1655 | {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, 1656 | ] 1657 | 1658 | [package.dependencies] 1659 | markdown-it-py = ">=2.2.0" 1660 | pygments = ">=2.13.0,<3.0.0" 1661 | 1662 | [package.extras] 1663 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 1664 | 1665 | [[package]] 1666 | name = "ruamel-yaml" 1667 | version = "0.18.6" 1668 | description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" 1669 | optional = false 1670 | python-versions = ">=3.7" 1671 | groups = ["test"] 1672 | files = [ 1673 | {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, 1674 | {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, 1675 | ] 1676 | 1677 | [package.dependencies] 1678 | "ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} 1679 | 1680 | [package.extras] 1681 | docs = ["mercurial (>5.7)", "ryd"] 1682 | jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] 1683 | 1684 | [[package]] 1685 | name = "ruamel-yaml-clib" 1686 | version = "0.2.8" 1687 | description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" 1688 | optional = false 1689 | python-versions = ">=3.6" 1690 | groups = ["test"] 1691 | markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\"" 1692 | files = [ 1693 | {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, 1694 | {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, 1695 | {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, 1696 | {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, 1697 | {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, 1698 | {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, 1699 | {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, 1700 | {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, 1701 | {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, 1702 | {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, 1703 | {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, 1704 | {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, 1705 | {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, 1706 | {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, 1707 | {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, 1708 | {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, 1709 | {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, 1710 | {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, 1711 | {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, 1712 | {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, 1713 | {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, 1714 | {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, 1715 | {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, 1716 | {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, 1717 | {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, 1718 | {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, 1719 | {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, 1720 | {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, 1721 | {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, 1722 | {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, 1723 | {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, 1724 | {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, 1725 | {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, 1726 | {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, 1727 | {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, 1728 | {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, 1729 | {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, 1730 | {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, 1731 | {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, 1732 | {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, 1733 | {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, 1734 | {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, 1735 | {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, 1736 | {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, 1737 | {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, 1738 | {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, 1739 | {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, 1740 | {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, 1741 | {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, 1742 | {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, 1743 | ] 1744 | 1745 | [[package]] 1746 | name = "setuptools" 1747 | version = "72.1.0" 1748 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 1749 | optional = false 1750 | python-versions = ">=3.8" 1751 | groups = ["test"] 1752 | files = [ 1753 | {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, 1754 | {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, 1755 | ] 1756 | 1757 | [package.extras] 1758 | core = ["importlib-metadata (>=6) ; python_version < \"3.10\"", "importlib-resources (>=5.10.2) ; python_version < \"3.9\"", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] 1759 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 1760 | test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-ruff (<0.4) ; platform_system == \"Windows\"", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "pytest-ruff (>=0.3.2) ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 1761 | 1762 | [[package]] 1763 | name = "six" 1764 | version = "1.16.0" 1765 | description = "Python 2 and 3 compatibility utilities" 1766 | optional = false 1767 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1768 | groups = ["test"] 1769 | files = [ 1770 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1771 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1772 | ] 1773 | 1774 | [[package]] 1775 | name = "snowballstemmer" 1776 | version = "2.2.0" 1777 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 1778 | optional = false 1779 | python-versions = "*" 1780 | groups = ["test"] 1781 | files = [ 1782 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 1783 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 1784 | ] 1785 | 1786 | [[package]] 1787 | name = "sortedcontainers" 1788 | version = "2.4.0" 1789 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 1790 | optional = false 1791 | python-versions = "*" 1792 | groups = ["test"] 1793 | files = [ 1794 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, 1795 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, 1796 | ] 1797 | 1798 | [[package]] 1799 | name = "stevedore" 1800 | version = "5.2.0" 1801 | description = "Manage dynamic plugins for Python applications" 1802 | optional = false 1803 | python-versions = ">=3.8" 1804 | groups = ["test"] 1805 | files = [ 1806 | {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"}, 1807 | {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"}, 1808 | ] 1809 | 1810 | [package.dependencies] 1811 | pbr = ">=2.0.0,<2.1.0 || >2.1.0" 1812 | 1813 | [[package]] 1814 | name = "strenum" 1815 | version = "0.4.15" 1816 | description = "An Enum that inherits from str." 1817 | optional = false 1818 | python-versions = "*" 1819 | groups = ["test"] 1820 | files = [ 1821 | {file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"}, 1822 | {file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"}, 1823 | ] 1824 | 1825 | [package.extras] 1826 | docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"] 1827 | release = ["twine"] 1828 | test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] 1829 | 1830 | [[package]] 1831 | name = "text-unidecode" 1832 | version = "1.3" 1833 | description = "The most basic Text::Unidecode port" 1834 | optional = false 1835 | python-versions = "*" 1836 | groups = ["test"] 1837 | files = [ 1838 | {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, 1839 | {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, 1840 | ] 1841 | 1842 | [[package]] 1843 | name = "toml" 1844 | version = "0.10.2" 1845 | description = "Python Library for Tom's Obvious, Minimal Language" 1846 | optional = false 1847 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 1848 | groups = ["test"] 1849 | files = [ 1850 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1851 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1852 | ] 1853 | 1854 | [[package]] 1855 | name = "tomli" 1856 | version = "2.0.1" 1857 | description = "A lil' TOML parser" 1858 | optional = false 1859 | python-versions = ">=3.7" 1860 | groups = ["test"] 1861 | markers = "python_full_version <= \"3.11.0a6\"" 1862 | files = [ 1863 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1864 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1865 | ] 1866 | 1867 | [[package]] 1868 | name = "tomlkit" 1869 | version = "0.13.0" 1870 | description = "Style preserving TOML library" 1871 | optional = false 1872 | python-versions = ">=3.8" 1873 | groups = ["test"] 1874 | files = [ 1875 | {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, 1876 | {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, 1877 | ] 1878 | 1879 | [[package]] 1880 | name = "types-cffi" 1881 | version = "1.16.0.20240331" 1882 | description = "Typing stubs for cffi" 1883 | optional = false 1884 | python-versions = ">=3.8" 1885 | groups = ["test"] 1886 | files = [ 1887 | {file = "types-cffi-1.16.0.20240331.tar.gz", hash = "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee"}, 1888 | {file = "types_cffi-1.16.0.20240331-py3-none-any.whl", hash = "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0"}, 1889 | ] 1890 | 1891 | [package.dependencies] 1892 | types-setuptools = "*" 1893 | 1894 | [[package]] 1895 | name = "types-mock" 1896 | version = "5.2.0.20250516" 1897 | description = "Typing stubs for mock" 1898 | optional = false 1899 | python-versions = ">=3.9" 1900 | groups = ["test"] 1901 | files = [ 1902 | {file = "types_mock-5.2.0.20250516-py3-none-any.whl", hash = "sha256:e50fbd0c3be8bcea25c30a47fac0b7a6ca22f630ef2f53416a73b319b39dfde1"}, 1903 | {file = "types_mock-5.2.0.20250516.tar.gz", hash = "sha256:aab7d3d9ad3814f2f8da12cc8e42d9be7d38200c5f214e3c0278c38fa01299d7"}, 1904 | ] 1905 | 1906 | [[package]] 1907 | name = "types-pyopenssl" 1908 | version = "24.1.0.20240722" 1909 | description = "Typing stubs for pyOpenSSL" 1910 | optional = false 1911 | python-versions = ">=3.8" 1912 | groups = ["test"] 1913 | files = [ 1914 | {file = "types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39"}, 1915 | {file = "types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54"}, 1916 | ] 1917 | 1918 | [package.dependencies] 1919 | cryptography = ">=35.0.0" 1920 | types-cffi = "*" 1921 | 1922 | [[package]] 1923 | name = "types-redis" 1924 | version = "4.6.0.20241004" 1925 | description = "Typing stubs for redis" 1926 | optional = false 1927 | python-versions = ">=3.8" 1928 | groups = ["test"] 1929 | files = [ 1930 | {file = "types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e"}, 1931 | {file = "types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed"}, 1932 | ] 1933 | 1934 | [package.dependencies] 1935 | cryptography = ">=35.0.0" 1936 | types-pyOpenSSL = "*" 1937 | 1938 | [[package]] 1939 | name = "types-setuptools" 1940 | version = "71.1.0.20240806" 1941 | description = "Typing stubs for setuptools" 1942 | optional = false 1943 | python-versions = ">=3.8" 1944 | groups = ["test"] 1945 | files = [ 1946 | {file = "types-setuptools-71.1.0.20240806.tar.gz", hash = "sha256:ae5e7b4d643ab9e99fc00ac00041804118cabe72a56183c30d524fb064897ad6"}, 1947 | {file = "types_setuptools-71.1.0.20240806-py3-none-any.whl", hash = "sha256:3bd8dd02039be0bb79ad880d8893b8eefcb022fabbeeb61245c61b20c9ab1ed0"}, 1948 | ] 1949 | 1950 | [[package]] 1951 | name = "typing-extensions" 1952 | version = "4.13.2" 1953 | description = "Backported and Experimental Type Hints for Python 3.8+" 1954 | optional = false 1955 | python-versions = ">=3.8" 1956 | groups = ["main", "test"] 1957 | files = [ 1958 | {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, 1959 | {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, 1960 | ] 1961 | 1962 | [[package]] 1963 | name = "url-normalize" 1964 | version = "1.4.3" 1965 | description = "URL normalization for Python" 1966 | optional = false 1967 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 1968 | groups = ["test"] 1969 | files = [ 1970 | {file = "url-normalize-1.4.3.tar.gz", hash = "sha256:d23d3a070ac52a67b83a1c59a0e68f8608d1cd538783b401bc9de2c0fac999b2"}, 1971 | {file = "url_normalize-1.4.3-py2.py3-none-any.whl", hash = "sha256:ec3c301f04e5bb676d333a7fa162fa977ad2ca04b7e652bfc9fac4e405728eed"}, 1972 | ] 1973 | 1974 | [package.dependencies] 1975 | six = "*" 1976 | 1977 | [[package]] 1978 | name = "urllib3" 1979 | version = "2.2.2" 1980 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1981 | optional = false 1982 | python-versions = ">=3.8" 1983 | groups = ["test"] 1984 | files = [ 1985 | {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, 1986 | {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, 1987 | ] 1988 | 1989 | [package.extras] 1990 | brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 1991 | h2 = ["h2 (>=4,<5)"] 1992 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1993 | zstd = ["zstandard (>=0.18.0)"] 1994 | 1995 | [[package]] 1996 | name = "wemake-python-styleguide" 1997 | version = "0.19.2" 1998 | description = "The strictest and most opinionated python linter ever" 1999 | optional = false 2000 | python-versions = "<4.0,>=3.9" 2001 | groups = ["test"] 2002 | files = [ 2003 | {file = "wemake_python_styleguide-0.19.2-py3-none-any.whl", hash = "sha256:d53205dbb629755026d853d15fb3ca03ebb2717c97de4198b5676b9bdc0663bd"}, 2004 | {file = "wemake_python_styleguide-0.19.2.tar.gz", hash = "sha256:850fe70e6d525fd37ac51778e552a121a489f1bd057184de96ffd74a09aef414"}, 2005 | ] 2006 | 2007 | [package.dependencies] 2008 | astor = ">=0.8,<0.9" 2009 | attrs = "*" 2010 | darglint = ">=1.2,<2.0" 2011 | flake8 = ">=7.0,<8.0" 2012 | flake8-bandit = ">=4.1,<5.0" 2013 | flake8-broken-line = ">=1.0,<2.0" 2014 | flake8-bugbear = ">=24.2,<25.0" 2015 | flake8-commas = ">=2.0,<3.0" 2016 | flake8-comprehensions = ">=3.1,<4.0" 2017 | flake8-debugger = ">=4.0,<5.0" 2018 | flake8-docstrings = ">=1.3,<2.0" 2019 | flake8-eradicate = ">=1.5,<2.0" 2020 | flake8-isort = ">=6.0,<7.0" 2021 | flake8-quotes = ">=3.0,<4.0" 2022 | flake8-rst-docstrings = ">=0.3,<0.4" 2023 | flake8-string-format = ">=0.3,<0.4" 2024 | pep8-naming = ">=0.13,<0.14" 2025 | pygments = ">=2.4,<3.0" 2026 | setuptools = "*" 2027 | typing_extensions = ">=4.0,<5.0" 2028 | 2029 | [[package]] 2030 | name = "win32-setctime" 2031 | version = "1.1.0" 2032 | description = "A small Python utility to set file creation time on Windows" 2033 | optional = false 2034 | python-versions = ">=3.5" 2035 | groups = ["test"] 2036 | markers = "sys_platform == \"win32\"" 2037 | files = [ 2038 | {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, 2039 | {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, 2040 | ] 2041 | 2042 | [package.extras] 2043 | dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] 2044 | 2045 | [[package]] 2046 | name = "zipp" 2047 | version = "3.19.2" 2048 | description = "Backport of pathlib-compatible object wrapper for zip files" 2049 | optional = false 2050 | python-versions = ">=3.8" 2051 | groups = ["test"] 2052 | markers = "python_version < \"3.10\"" 2053 | files = [ 2054 | {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, 2055 | {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, 2056 | ] 2057 | 2058 | [package.extras] 2059 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 2060 | test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] 2061 | 2062 | [extras] 2063 | aioredis = ["aioredis"] 2064 | dev = ["aioredis", "redis"] 2065 | redis = ["redis"] 2066 | 2067 | [metadata] 2068 | lock-version = "2.1" 2069 | python-versions = "^3.9" 2070 | content-hash = "d96a16a4c5196d1c51118fce69ed8de38a5551525c11268a66aad453fcd1a7de" 2071 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "asyncio-redis-rate-limit" 3 | description = "Rate limiter for async functions using Redis as a backend" 4 | version = "1.0.0" 5 | license = "MIT" 6 | readme = "README.md" 7 | 8 | authors = [ 9 | "Nikita Sobolev " 10 | ] 11 | 12 | repository = "https://github.com/wemake-services/asyncio-redis-rate-limit" 13 | 14 | keywords = [ 15 | "asyncio", 16 | "rate-limiter", 17 | "redis", 18 | "redis-py", 19 | "aioredis", 20 | ] 21 | 22 | classifiers = [ 23 | "Development Status :: 4 - Beta", 24 | "Intended Audience :: Developers", 25 | "Operating System :: OS Independent", 26 | "Topic :: Software Development :: Libraries :: Python Modules", 27 | ] 28 | 29 | [tool.poetry.urls] 30 | "Funding" = "https://github.com/sponsors/wemake-services" 31 | 32 | [tool.poetry.dependencies] 33 | python = "^3.9" 34 | 35 | redis = { version = ">=4.5,<7", optional = true } 36 | # aioredis and python3.11 are not compatible: 37 | aioredis = { version = ">=2.0", optional = true, python = "<3.11" } 38 | typing-extensions = ">=3.10" 39 | 40 | [tool.poetry.group.test.dependencies] 41 | mypy = "^1.11" 42 | types-redis = "^4.6" 43 | types-mock = "^5.0" 44 | 45 | wemake-python-styleguide = "^0.19" 46 | flake8-pytest-style = "^2.0" 47 | nitpick = "^0.35" 48 | 49 | pytest = "^8.3" 50 | pytest-cov = "6.1.1" 51 | pytest-randomly = "^3.12" 52 | pytest-asyncio = ">=0.23,<0.27" 53 | pytest-freezegun = "^0.4" 54 | pytest-repeat = "^0.9" 55 | 56 | [tool.poetry.extras] 57 | redis = ["redis"] 58 | aioredis = ["aioredis"] 59 | dev = ["redis", "aioredis"] 60 | 61 | 62 | [build-system] 63 | requires = ["poetry-core>=1.9.0"] 64 | build-backend = "poetry.core.masonry.api" 65 | 66 | 67 | [tool.nitpick] 68 | style = "https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/0.19.2/styles/nitpick-style-wemake.toml" 69 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # All configuration for plugins and other utils is defined here. 2 | # Read more about `setup.cfg`: 3 | # https://docs.python.org/3/distutils/configfile.html 4 | 5 | [flake8] 6 | # Base flake8 configuration: 7 | # https://flake8.pycqa.org/en/latest/user/configuration.html 8 | format = wemake 9 | show-source = true 10 | statistics = false 11 | doctests = true 12 | 13 | # darglint configuration: 14 | # https://github.com/terrencepreilly/darglint 15 | strictness = long 16 | docstring-style = numpy 17 | 18 | # Plugins: 19 | max-complexity = 6 20 | max-line-length = 80 21 | 22 | # wemake-python-styleguide settings: 23 | i-control-code = false 24 | 25 | # Disable some pydocstyle checks: 26 | # Exclude some pydoctest checks globally: 27 | ignore = D100, D104, D106, D401, W504, X100, RST303, RST304, DAR103, DAR203 28 | 29 | # Excluding some directories: 30 | exclude = 31 | .git 32 | __pycache__ 33 | .venv 34 | .eggs 35 | *.egg 36 | 37 | # Ignoring some errors in some files: 38 | per-file-ignores = 39 | # Enable `assert` keyword and magic numbers for tests: 40 | tests/*.py: S101, WPS202, WPS204, WPS217, WPS226, WPS432, WPS442 41 | 42 | 43 | [isort] 44 | # isort configuration: 45 | # https://github.com/timothycrosley/isort/wiki/isort-Settings 46 | profile = wemake 47 | 48 | 49 | [tool:pytest] 50 | # Treat async tests as pytest-tests: 51 | asyncio_mode = auto 52 | asyncio_default_fixture_loop_scope = function 53 | 54 | # Directories that are not visited by pytest collector: 55 | norecursedirs = *.egg .eggs dist build docs .tox .git __pycache__ 56 | 57 | # Strict `@xfail` by default: 58 | xfail_strict = true 59 | 60 | # Extra options: 61 | addopts = 62 | --strict-markers 63 | --strict-config 64 | --tb=short 65 | --cov=asyncio_redis_rate_limit 66 | --cov-report=term-missing:skip-covered 67 | --cov-report=html 68 | --cov-report=xml 69 | --cov-branch 70 | --cov-fail-under=100 71 | 72 | 73 | [coverage:run] 74 | # Coverage configuration: https://coverage.readthedocs.io/ 75 | 76 | # It is hard to cover this module. 77 | # TODO: use https://github.com/wemake-services/coverage-conditional-plugin 78 | omit = 79 | asyncio_redis_rate_limit/compat.py 80 | 81 | 82 | [mypy] 83 | # mypy configurations: http://bit.ly/2zEl9WI 84 | enable_error_code = 85 | truthy-bool, 86 | truthy-iterable, 87 | redundant-expr, 88 | unused-awaitable, 89 | # ignore-without-code, 90 | possibly-undefined, 91 | redundant-self, 92 | 93 | ignore_missing_imports = true 94 | strict = true 95 | warn_unreachable = true 96 | 97 | [mypy-asyncio_redis_rate_limit.compat] 98 | # Some ignores are bound to existing packages: 99 | allow_any_explicit = true 100 | warn_unused_ignores = false 101 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from typing import AsyncGenerator, Awaitable, cast 4 | 5 | import pytest 6 | from typing_extensions import Final, Protocol 7 | 8 | from asyncio_redis_rate_limit import RateLimitError, RateSpec, rate_limit 9 | from asyncio_redis_rate_limit.compat import ( # type: ignore # noqa: WPS450 10 | HAS_AIOREDIS, 11 | HAS_REDIS, 12 | AnyRedis, 13 | _AIORedis, 14 | _AsyncRedis, 15 | ) 16 | 17 | 18 | class _LimitedSig(Protocol): 19 | def __call__(self, number: int = ...) -> Awaitable[int]: 20 | """We use this to assert the output.""" 21 | 22 | 23 | class _LimitedCallback(Protocol): 24 | def __call__( 25 | self, 26 | requests: int = ..., 27 | seconds: int = ..., 28 | ) -> _LimitedSig: 29 | """We use this callback to construct `limited` test function.""" 30 | 31 | 32 | _LIMIT: Final = 5 33 | _SECONDS: Final = 1 34 | 35 | 36 | @pytest.fixture(params=[_AsyncRedis, _AIORedis]) 37 | async def redis(request: pytest.FixtureRequest) -> AnyRedis: 38 | """Creates an async redis client.""" 39 | if issubclass(request.param, _AsyncRedis) and not HAS_REDIS: 40 | pytest.skip('`redis` is not installed') 41 | elif issubclass(request.param, _AIORedis) and not HAS_AIOREDIS: 42 | pytest.skip('`aioredis` is not installed') 43 | 44 | return cast(AnyRedis, request.param.from_url( 45 | 'redis://{0}:6379'.format(os.environ.get('REDIS_HOST', 'localhost')), 46 | )) 47 | 48 | 49 | @pytest.fixture(autouse=True) 50 | async def _clear_redis(redis: AnyRedis) -> AsyncGenerator[None, None]: 51 | """This fixture is needed to be sure that test start with fresh redis.""" 52 | yield 53 | await redis.flushdb() 54 | 55 | 56 | @pytest.fixture 57 | def limited(redis: AnyRedis) -> _LimitedCallback: 58 | """Fixture to construct rate limited functions.""" 59 | def factory( 60 | requests: int = _LIMIT, 61 | seconds: int = _SECONDS, 62 | ) -> _LimitedSig: 63 | @rate_limit( 64 | rate_spec=RateSpec(requests=requests, seconds=seconds), 65 | backend=redis, 66 | ) 67 | async def decorator(index: int = 0) -> int: 68 | return index 69 | return decorator # type: ignore[return-value] 70 | return factory 71 | 72 | 73 | @pytest.mark.repeat(5) 74 | async def test_correct(limited: _LimitedCallback) -> None: 75 | """Ensure that coroutine under limit always works correctly.""" 76 | function = limited() 77 | for attempt in range(_LIMIT): 78 | assert await function(attempt) == attempt 79 | 80 | # Next attempt will raise: 81 | with pytest.raises(RateLimitError): 82 | await function() 83 | 84 | #: Freezing time on a client won't help, server's time is important. 85 | test_correct_frozen = pytest.mark.freeze_time('2020-02-03')(test_correct) 86 | 87 | 88 | @pytest.mark.parametrize(('limit', 'seconds'), [ 89 | (_LIMIT, _SECONDS), 90 | (5, 2), 91 | (3, 3), 92 | (1, 2), 93 | ]) 94 | async def test_with_sleep( 95 | limited: _LimitedCallback, 96 | limit: int, 97 | seconds: int, 98 | ) -> None: 99 | """Ensure that when time passes, limit is restored.""" 100 | function = limited(seconds=seconds, requests=limit) 101 | for attempt in range(limit): 102 | assert await function(attempt) == attempt 103 | 104 | # Next attempt will raise: 105 | with pytest.raises(RateLimitError): 106 | await function() 107 | 108 | # Sleep for a while: 109 | await asyncio.sleep(seconds + 0.5) # for extra safety 110 | 111 | # Next attempt will not raise, since some time has passed: 112 | await function() 113 | 114 | 115 | async def test_different_functions( 116 | limited: _LimitedCallback, 117 | redis: AnyRedis, 118 | ) -> None: 119 | """Ensure that unrelated functions are unrelated.""" 120 | @rate_limit( 121 | rate_spec=RateSpec(requests=5, seconds=1), 122 | backend=redis, 123 | ) 124 | async def factory(index: int = 0) -> int: 125 | return index 126 | 127 | for attempt in range(_LIMIT): 128 | await factory(attempt) 129 | 130 | # Next attempt will raise: 131 | with pytest.raises(RateLimitError): 132 | await factory() 133 | 134 | # Unrelated function should be fine: 135 | factory2 = limited() 136 | for attempt2 in range(5): 137 | await factory2(attempt2) 138 | 139 | # Next attempt will raise: 140 | with pytest.raises(RateLimitError): 141 | await factory2() 142 | 143 | 144 | async def test_different_prefixes(redis: AnyRedis) -> None: 145 | """Ensure that different prefixes work for the same function.""" 146 | async def factory(index: int = 0) -> int: 147 | return index 148 | 149 | limited1 = rate_limit( 150 | rate_spec=RateSpec(requests=5, seconds=1), 151 | backend=redis, 152 | cache_prefix='one', 153 | )(factory) 154 | 155 | for attempt in range(_LIMIT): 156 | await limited1(attempt) 157 | 158 | # Next attempt will raise: 159 | with pytest.raises(RateLimitError): 160 | await limited1() 161 | 162 | # Different prefix should be fine: 163 | limited2 = rate_limit( 164 | rate_spec=RateSpec(requests=5, seconds=1), 165 | backend=redis, 166 | cache_prefix='two', 167 | )(factory) 168 | 169 | for attempt2 in range(5): 170 | await limited2(attempt2) 171 | 172 | # Next attempt will raise: 173 | with pytest.raises(RateLimitError): 174 | await limited2() 175 | 176 | 177 | @pytest.mark.repeat(3) 178 | async def test_gather_correct(limited: _LimitedCallback) -> None: 179 | """Ensure that several gathered coroutines do respect the rate limit.""" 180 | function = limited() 181 | 182 | assert await asyncio.gather(*[ 183 | function(attempt) 184 | for attempt in range(_LIMIT) 185 | ]) == [0, 1, 2, 3, 4] 186 | 187 | 188 | @pytest.mark.repeat(3) 189 | async def test_gather_limited(limited: _LimitedCallback) -> None: 190 | """Ensure gathered coroutines can be rate limited.""" 191 | function = limited() 192 | 193 | with pytest.raises(RateLimitError): 194 | await asyncio.gather(*[ 195 | function(attempt) 196 | for attempt in range(_LIMIT + 1) 197 | ]) 198 | 199 | 200 | @pytest.mark.repeat(5) 201 | async def test_ten_reqs_in_two_secs( 202 | limited: _LimitedCallback, 203 | ) -> None: 204 | """Ensure that several gathered coroutines do respect the rate limit.""" 205 | function = limited(requests=10, seconds=2) 206 | 207 | # At first, try 5 requests, a half: 208 | for attempt in range(5): 209 | await function(attempt) 210 | 211 | # Now, let's move time to the next second: 212 | await asyncio.sleep(1) 213 | 214 | # Other 5 should be fine: 215 | for attempt2 in range(5): 216 | await function(attempt2) 217 | 218 | # This one will fail: 219 | with pytest.raises(RateLimitError): 220 | await function() 221 | 222 | 223 | @pytest.mark.repeat(5) 224 | async def test_ten_reqs_in_two_secs2( 225 | limited: _LimitedCallback, 226 | ) -> None: 227 | """Ensure that several gathered coroutines do respect the rate limit.""" 228 | function = limited(requests=10, seconds=2) 229 | 230 | # Or just consume all: 231 | for attempt in range(10): 232 | await function(attempt) 233 | 234 | # This one will fail: 235 | with pytest.raises(RateLimitError): 236 | await function() 237 | 238 | # Now, let's move time to the next second: 239 | await asyncio.sleep(1) 240 | 241 | # This one will also fail: 242 | with pytest.raises(RateLimitError): 243 | await function() 244 | 245 | # Next attempts will pass: 246 | await asyncio.sleep(1 + 0.5) 247 | await function() 248 | 249 | 250 | class _Counter: 251 | def __init__(self) -> None: 252 | self.count = 0 253 | 254 | async def increment(self) -> None: 255 | self.count += 1 256 | 257 | 258 | @pytest.mark.repeat(5) 259 | async def test_that_rate_limit_do_not_cancel_others(redis: AnyRedis) -> None: 260 | """Ensure that when rate limit is hit, we still execute other requests.""" 261 | counter = _Counter() 262 | limited = rate_limit( 263 | rate_spec=RateSpec(requests=_LIMIT, seconds=2), 264 | backend=redis, 265 | cache_prefix='one', 266 | )(counter.increment) 267 | 268 | for attempt in range(_LIMIT + 1): 269 | if attempt == _LIMIT: 270 | with pytest.raises(RateLimitError): 271 | await limited() 272 | else: 273 | await limited() 274 | 275 | # But, we still do this work while rate limit is not met: 276 | assert counter.count == _LIMIT 277 | -------------------------------------------------------------------------------- /tests/test_multiprocess_aioredis.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import multiprocessing 3 | import os 4 | import time 5 | 6 | import pytest 7 | from typing_extensions import Final 8 | 9 | from asyncio_redis_rate_limit import RateLimitError, RateSpec, rate_limit 10 | from asyncio_redis_rate_limit.compat import ( # type: ignore # noqa: WPS450 11 | HAS_AIOREDIS, 12 | _AIORedis, 13 | ) 14 | 15 | if not HAS_AIOREDIS: 16 | pytest.skip('`aioredis` package is not installed', allow_module_level=True) 17 | 18 | _redis: Final = _AIORedis.from_url( 19 | 'redis://{0}:6379'.format(os.environ.get('REDIS_HOST', 'localhost')), 20 | ) 21 | _event_loop: Final = asyncio.new_event_loop() 22 | _LIMIT: Final = 5 23 | _SECONDS: Final = 1 24 | 25 | 26 | @pytest.fixture 27 | def event_loop() -> asyncio.AbstractEventLoop: 28 | """Overriding `pytest-asyncio` fixture.""" 29 | return _event_loop 30 | 31 | 32 | @rate_limit( 33 | rate_spec=RateSpec(requests=_LIMIT, seconds=_SECONDS), 34 | backend=_redis, 35 | cache_prefix='mp-aioredis', 36 | ) 37 | async def _limited(index: int) -> int: 38 | return index 39 | 40 | 41 | def _worker(number: int) -> int: 42 | return _event_loop.run_until_complete(_limited(number)) 43 | 44 | 45 | def test_multiprocess(event_loop: asyncio.BaseEventLoop) -> None: 46 | """Ensure that `multiprocessing` works with limits.""" 47 | with multiprocessing.Pool() as pool: 48 | reduced = pool.map(_worker, list(range(_LIMIT))) 49 | assert reduced == [0, 1, 2, 3, 4] 50 | 51 | time.sleep(_SECONDS) 52 | 53 | with multiprocessing.Pool() as pool2: 54 | with pytest.raises(RateLimitError): 55 | pool2.map(_worker, list(range(_LIMIT + 1))) 56 | -------------------------------------------------------------------------------- /tests/test_multiprocess_redis.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import multiprocessing 3 | import os 4 | import time 5 | 6 | import pytest 7 | from typing_extensions import Final 8 | 9 | from asyncio_redis_rate_limit import RateLimitError, RateSpec, rate_limit 10 | from asyncio_redis_rate_limit.compat import ( # type: ignore # noqa: WPS450 11 | HAS_REDIS, 12 | _AsyncRedis, 13 | ) 14 | 15 | if not HAS_REDIS: 16 | pytest.skip('`redis` package is not installed', allow_module_level=True) 17 | 18 | _redis: Final = _AsyncRedis.from_url( 19 | 'redis://{0}:6379'.format(os.environ.get('REDIS_HOST', 'localhost')), 20 | ) 21 | _event_loop: Final = asyncio.new_event_loop() 22 | _LIMIT: Final = 5 23 | _SECONDS: Final = 1 24 | 25 | 26 | @pytest.fixture 27 | def event_loop() -> asyncio.AbstractEventLoop: 28 | """Overriding `pytest-asyncio` fixture.""" 29 | return _event_loop 30 | 31 | 32 | @rate_limit( 33 | rate_spec=RateSpec(requests=_LIMIT, seconds=_SECONDS), 34 | backend=_redis, 35 | cache_prefix='mp-redis', 36 | ) 37 | async def _limited(index: int) -> int: 38 | return index 39 | 40 | 41 | def _worker(number: int) -> int: 42 | return _event_loop.run_until_complete(_limited(number)) 43 | 44 | 45 | def test_multiprocess(event_loop: asyncio.BaseEventLoop) -> None: 46 | """Ensure that `multiprocessing` works with limits.""" 47 | with multiprocessing.Pool() as pool: 48 | reduced = pool.map(_worker, list(range(_LIMIT))) 49 | assert reduced == [0, 1, 2, 3, 4] 50 | 51 | time.sleep(_SECONDS) 52 | 53 | with multiprocessing.Pool() as pool2: 54 | with pytest.raises(RateLimitError): 55 | pool2.map(_worker, list(range(_LIMIT + 1))) 56 | --------------------------------------------------------------------------------