pax_global_header00006660000000000000000000000064145601500160014510gustar00rootroot0000000000000052 comment=6547679463b6a6fa87d9e254a8558bbc6075e263 slowapi-0.1.9/000077500000000000000000000000001456015001600131755ustar00rootroot00000000000000slowapi-0.1.9/.github/000077500000000000000000000000001456015001600145355ustar00rootroot00000000000000slowapi-0.1.9/.github/ISSUE_TEMPLATE/000077500000000000000000000000001456015001600167205ustar00rootroot00000000000000slowapi-0.1.9/.github/ISSUE_TEMPLATE/bug-report---something-is-not-working.md000066400000000000000000000013371456015001600263460ustar00rootroot00000000000000--- name: Bug report - Something is not working about: You found a bug, or your code is not working as you expect title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Show us your code, only copy the relevant parts, the shorter it is, the easier it is to help you. **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Your app (please complete the following information):** - fastapi or starlette? - Version? - slowapi version (have you tried with the latest version)? **Additional context** Add any other context about the problem here. slowapi-0.1.9/.github/workflows/000077500000000000000000000000001456015001600165725ustar00rootroot00000000000000slowapi-0.1.9/.github/workflows/python-package.yml000066400000000000000000000030261456015001600222300ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python package on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install build dependencies for requests in python 3.9 # it's not clear why this is needed only for this version of python run: sudo apt-get install libxml2-dev libxslt-dev - name: Install Poetry uses: snok/install-poetry@v1 with: # Version of Poetry to use version: 1.4.2 - name: Install dependencies run: | poetry install - name: Check formatting with black run: | poetry run black --check . - name: Check typing annotations with mypy run: | poetry run mypy . - name: Verify unused imports run: | poetry run flake8 --select F401 - name: Test with pytest # Wrapped by coverage to generate coverage data run: | poetry run coverage run --omit="tests*" -m pytest - name: Generate coverage report run: | poetry run coverage report slowapi-0.1.9/.gitignore000066400000000000000000000001401456015001600151600ustar00rootroot00000000000000# testing .pytest_cache __pycache__ # typing .mypy_cache # editors .idea # coverage .coverageslowapi-0.1.9/.readthedocs.yml000066400000000000000000000007221456015001600162640ustar00rootroot00000000000000# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Build documentation with MkDocs mkdocs: configuration: mkdocs.yml # Optionally build your docs in additional formats such as PDF formats: - pdf # Optionally set the version of Python and requirements required to build your docs python: version: 3.7 install: - requirements: docs/requirements.txt slowapi-0.1.9/CHANGELOG.md000066400000000000000000000055351456015001600150160ustar00rootroot00000000000000# Change Log ## [0.1.9] - 2024-02-05 ### Added - Fix `limit_value` typehint in `limit()` function (thanks @PookieBuns) - Fix `limit_value` typehint in `shared_limit()` function (thanks @aberlioz) - Only pass `".env"` to starlette `Config` if `".env"` file exists (thanks @daniellok-db) ## [0.1.8] - 2023-04-07 ### Added - Loosen restriction on the limits dependency (thanks @sanders41) - Fix redis install error (thanks @sanders41) - Add Python 3.11 support (thanks @sanders41) ## [0.1.7] - 2022-11-09 ### Added - Added ASGI middleware alternative (thanks @thentgesMindee) - Added support for custom cost per hit (thanks @nootr) - Added `key_style` parameter to choose between endpoint or url (thanks @thentgesMindee) ## [0.1.6] - 2022-08-20 ### Added - Added feature to support providing functions for dynamically defined limits (thanks @maratsarbasov) - Added github action to check for unused imports (thanks @twcurrie) - Added coverage report in CI (thanks @karlnewell) - Added Python 3.10 to CI (thanks @Reuben Thomas-Davis) ### Changed - Shifted redis to extras, removed test imports of library (thanks @ME-ON1) - Upgraded dependencies (thanks @dependabot, @Rested, @laurents) - Updated documentation and example code (thanks @Dustyposa, @laurents, @nootr) - Set minimum Python version to 3.6.2 (thanks @Rested) ### Fixed - Fixed exempt decorator for async routes (thanks @laurents) - Handled newly raised exception from parsing library (thanks @Rested) ## [0.1.5] - 2021-08-28 ### Changed - Switched to poetry-core for building #54 (thanks @fabaff) - Improved the docs - Upgraded a few dependencies (thanks @dependabot) ### Fixed - Resolved bug of unregistered endpoints in the disabled state #46 (thanks @twcurrie) - Fixed bug with Retry-After headers #60 (thanks again @twcurrie) ## [0.1.4] - 2021-02-21 - Made the enabled option actually useful (thanks @kodekracker for the report) #35 - Fixed 2 bugs in middleware #30 and #37 (thanks @xuxygo for the PR, and @papapumpnz for the report) - Fixed errors in docs - Bump lxml to 4.6.2 (dependabot - only used for doc generation) ## [0.1.3] - 2020-12-24 ### Added - Added some setup examples in documentation ### Fixed - Routes returning a dict don't error when turning on headers (#18), thanks to @glinmac - Fix CI crash following github actions changes in env settings ## [0.1.2] - 2020-10-01 ### Added - Added support for default limits and exempt routes, thanks to @Rested - Added documentation - Added more tests, thanks to @thomasleveil - Fix documentation bug, thanks to @brumar - Added CI checks for formatting, typing and tests ### Changed - Upgraded supported version of Starlette (0.13.6) and FastApi (0.61.1) ## [0.1.1] - 2020-03-11 ### Added - Added explicit support for typing ### Changed - Upgraded supported version of Starlette (0.13.2) and FastApi (0.52.0) ## [0.1.0] - 2020-02-21 Initial release slowapi-0.1.9/LICENSE000066400000000000000000000020601456015001600142000ustar00rootroot00000000000000MIT License Copyright (c) 2020 Laurent Savaete Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. slowapi-0.1.9/README.md000066400000000000000000000040131456015001600144520ustar00rootroot00000000000000# SlowApi A rate limiting library for Starlette and FastAPI adapted from [flask-limiter](http://github.com/alisaifee/flask-limiter). This package is used in various production setups, handling millions of requests per month, and seems to behave as expected. There might be some API changes when changing the code to be fully `async`, but we will notify users via appropriate `semver` version changes. The documentation is on [read the docs](https://slowapi.readthedocs.io/en/latest/). # Quick start ## Installation `slowapi` is available from [pypi](https://pypi.org/project/slowapi/) so you can install it as usual: ``` $ pip install slowapi ``` # Features Most feature are coming from FlaskLimiter and the underlying [limits](https://limits.readthedocs.io/). Supported now: - Single and multiple `limit` decorator on endpoint functions to apply limits - redis, memcached and memory backends to track your limits (memory as a fallback) - support for sync and async HTTP endpoints - Support for shared limits across a set of routes # Limitations and known issues * The `request` argument must be explicitly passed to your endpoint, or `slowapi` won't be able to hook into it. In other words, write: ```python @limiter.limit("5/minute") async def myendpoint(request: Request) pass ``` and not: ```python @limiter.limit("5/minute") async def myendpoint() pass ``` * `websocket` endpoints are not supported yet. # Developing and contributing PRs are more than welcome! Please include tests for your changes :) The package uses [poetry](https://python-poetry.org) to manage dependencies. To setup your dev env: ```bash $ poetry install ``` To run the tests: ```bash $ pytest ``` # Credits Credits go to [flask-limiter](https://github.com/alisaifee/flask-limiter) of which SlowApi is a (still partial) adaptation to Starlette and FastAPI. It's also important to mention that the actual rate limiting work is done by [limits](https://github.com/alisaifee/limits/), `slowapi` is just a wrapper around it. slowapi-0.1.9/docs/000077500000000000000000000000001456015001600141255ustar00rootroot00000000000000slowapi-0.1.9/docs/api.md000066400000000000000000000006751456015001600152300ustar00rootroot00000000000000# API reference ## Limiter class :::slowapi.extension.Limiter :docstring: :members: limit shared_limit ## Wrappers around Limit objects These wrap the `RateLimitItem` from [alisaifee/limits](https://limits.readthedocs.io/). :::slowapi.wrappers.Limit :docstring: :::slowapi.wrappers.LimitGroup :docstring: ## Utility functions :::slowapi.util.get_ipaddr :docstring: :::slowapi.util.get_remote_address :docstring: slowapi-0.1.9/docs/css/000077500000000000000000000000001456015001600147155ustar00rootroot00000000000000slowapi-0.1.9/docs/css/custom.css000066400000000000000000000002731456015001600167430ustar00rootroot00000000000000div.autodoc-docstring { padding-left: 20px; margin-bottom: 30px; border-left: 5px solid rgba(230, 230, 230); } div.autodoc-members { padding-left: 20px; margin-bottom: 15px; } slowapi-0.1.9/docs/examples.md000066400000000000000000000110711456015001600162650ustar00rootroot00000000000000# Examples Here are some examples of setup to get you started. Please open an issue if you have a use case that is not included here. The tests show a lot of different use cases that are not all covered here. ## Apply a global (default) limit to all routes ```python from starlette.applications import Starlette from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.middleware import SlowAPIMiddleware from slowapi.errors import RateLimitExceeded limiter = Limiter(key_func=get_remote_address, default_limits=["1/minute"]) app = Starlette() app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware(SlowAPIMiddleware) # this will be limited by the default_limits async def homepage(request: Request): return PlainTextResponse("Only once per minute") app.add_route("/home", homepage) ``` ## Exempt a route from the global limit ```python @app.route("/someroute") @limiter.exempt def t(request: Request): return PlainTextResponse("I'm unlimited") ``` ## Disable the limiter entirely You might want to disable the limiter, for instance for testing, etc... Note that this disables it entirely, for all users. It is essentially as if the limiter was not there. Simply pass `enabled=False` to the constructor. ```python limiter = Limiter(key_func=get_remote_address, enabled=False) @app.route("/someroute") @limiter.exempt def t(request: Request): return PlainTextResponse("I'm unlimited") ``` You can always switch this during the lifetime of the limiter: ```python limiter.enabled = False ``` ## Use redis as backend for the limiter ```python limiter = Limiter(key_func=get_remote_address, storage_uri="redis://:/n") ``` where the /n in the redis url is the database number. To use the default one, just drop the /n from the url. There are more examples in the [limits docs](https://limits.readthedocs.io/en/stable/storage.html) which is the library slowapi uses to manage storage. ## Set a custom cost per hit Setting a custom cost per hit is useful to throttle requests based on something else than the request count. Define a function which takes a request as parameter and returns a cost and pass it to the `limit` decorator: ```python def get_hit_cost(request: Request) -> int: return len(request) @app.route("/someroute") @limiter.limit("100/minute", cost=get_hit_cost) def t(request: Request): return PlainTextResponse("I'm limited by the request size") ``` ## WSGI vs ASGI Middleware `SlowAPIMiddleware` inheriting from Starlette's BaseHTTPMiddleware, you can find an alternative ASGI Middleware `SlowAPIASGIMiddleware`. A few reasons to choose the ASGI middleware over the HTTP one are: - Starlette [is probably going to deprecate BaseHTTPMiddleware](https://github.com/encode/starlette/issues/1678) - ASGI middlewares [are more performant than WSGI ones](https://github.com/tiangolo/fastapi/issues/2241) - built-in support for asynchronous exception handlers - ... Both middlewares are added to your application the same way: ```python app = Starlette() # or FastAPI() app.add_middleware(SlowAPIMiddleware) ``` or ```python app = Starlette() # or FastAPI() app.add_middleware(SlowAPIASGIMiddleware) ``` ## Use view function's name instead of full endpoint as part of the storage key Let's use this route as an example: ```python @app.route("/some_route/{some_param}") def my_func(some_param): ... ``` ```python limiter = Limiter(key_func=lambda: "mock", default_limits=["1/minute"], key_style="url") ``` When initializing the Limiter object with `key_style="url"`, it will use the full endpoint url as part of the storage key. When calling the `/some_route/my_param` endpoint would result with a key shaped like: `LIMITER/mock//some_route/my_param/1/1/minute`. > This means, that if the route contains some URL parameter, calling the endpoint with different parameters won't share the limitations. ```python limiter = Limiter(key_func=lambda: "mock", default_limits=["1/minute"], key_style="endpoint") ``` When initializing the Limiter object with `key_style="endpoint"`, it will use the function name as part of the storage key. When calling the `/some_route/my_param` endpoint would result with a key shaped like: `LIMITER/mock/{module}.my_func/1/1/minute` > This means, that if the route contains some URL parameter, calling the endpoint with different parameters will still share the limitations, since the view function is the same. slowapi-0.1.9/docs/index.md000066400000000000000000000116051456015001600155610ustar00rootroot00000000000000# SlowApi A rate limiting library for Starlette and FastAPI adapted from [flask-limiter](http://github.com/alisaifee/flask-limiter). Note: this is alpha quality code still, the API may change, and things may fall apart while you try it. # Quick start ## Installation `slowapi` is available from [pypi](https://pypi.org/project/slowapi/) so you can install it as usual: ``` $ pip install slowapi ``` ## Starlette ```python from starlette.applications import Starlette from starlette.responses import PlainTextResponse from starlette.requests import Request from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded limiter = Limiter(key_func=get_remote_address) app = Starlette() app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) @limiter.limit("5/minute") async def homepage(request: Request): return PlainTextResponse("test") app.add_route("/home", homepage) ``` The above app will have a route `t1` that will accept up to 5 requests per minute. Requests beyond this limit will be answered with an HTTP 429 error, and the body of the view will not run. ## FastAPI ```python from fastapi import FastAPI, Request, Response from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded limiter = Limiter(key_func=get_remote_address) app = FastAPI() app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # Note: the route decorator must be above the limit decorator, not below it @app.get("/home") @limiter.limit("5/minute") async def home(request: Request): return Response("test") @app.get("/mars") @limiter.limit("5/minute") async def mars(request: Request, response: Response): return {"key": "value"} ``` This will provide the same result, but with a FastAPI app. # Features Most feature are coming from (will come from) FlaskLimiter and the underlying [limits](https://limits.readthedocs.io/). Supported now: - Single and multiple `limit` decorator on endpoint functions to apply limits - Redis, memcached and memory backends to track your limits (memory as a fallback) - Support for sync and async HTTP endpoints - Support for shared limits across a set of routes - Support for default global limit - Support for a custom cost per hit # Limitations and known issues ## Request argument The `request` argument must be explicitly passed to your endpoint, or `slowapi` won't be able to hook into it. In other words, write: ```python @limiter.limit("5/minute") async def myendpoint(request: Request) pass ``` and not: ```python @limiter.limit("5/minute") async def myendpoint() pass ``` ## Response type Similarly, if the returned response is not an instance of `Response` and will be built at an upper level in the middleware stack, you'll need to provide the response object explicitly if you want the `Limiter` to modify the headers (`headers_enabled=True`): ```python @limiter.limit("5/minute") async def myendpoint(request: Request, response: Response) return {"key": "value"} ``` ## Decorators order The order of decorators matters. It is not a bug, the `limit` decorator needs the `request` argument in the function it decorates (see above). This works ``` @router.get("/test") @limiter.limit("2/minute") async def test( request: Request ): return "hi" ``` but this doesnt ``` @limiter.limit("2/minute") @router.get("/test") async def test( request: Request ): return "hi" ``` ## Websocket endpoints `websocket` endpoints are not supported yet. # Examples of setup See [examples](examples.md) # Developing and contributing PRs are more than welcome! Please include tests for your changes :) Please run [black](black.readthedocs.io/) on your code before committing, or your PR will not pass the tests. The package uses [poetry](https://python-poetry.org) to manage dependencies. To setup your dev env: ```bash $ poetry install ``` To run the tests: ```bash $ pytest ``` ## Releasing a new version `slowapi` tries to follow [semantic versioning](https://semver.org/). To release a new version: - Update CHANGELOG.md - Bump the version number in `pyproject.toml` - `poetry build` - `poetry publish` # Credits Credits go to [flask-limiter](https://github.com/alisaifee/flask-limiter) of which SlowApi is a (still partial) adaptation to Starlette and FastAPI. It's also important to mention that the actual rate limiting work is done by [limits](https://github.com/alisaifee/limits/), `slowapi` is just a wrapper around it. The documentation is built using [mkDocs](https://www.mkdocs.org/) and the API documentation is generated using [mkautodoc](https://github.com/tomchristie/mkautodoc). slowapi-0.1.9/docs/requirements.txt000066400000000000000000000017301456015001600174120ustar00rootroot00000000000000appdirs==1.4.3 atomicwrites==1.3.0; sys_platform == "win32" attrs==19.3.0 black==23.3.0 certifi==2022.12.7 chardet==3.0.4 click==7.1.2 colorama==0.4.3; sys_platform == "win32" dataclasses==0.6; python_version < "3.7" fastapi==0.65.2 future==0.18.3 hiro==0.5.1 idna==2.9 importlib-metadata==1.5.0; python_version < "3.8" isort==4.3.21 jinja2==2.11.3 joblib==0.16.0; python_version > "2.7" limits==1.5.1 livereload==2.6.3 lunr==0.5.8 lxml==4.9.1 markdown==3.2.2 markupsafe==1.1.1 mkautodoc==0.1.0 mkdocs==1.2.3 mock==4.0.1 more-itertools==8.2.0 mypy==0.761 mypy-extensions==0.4.3 nltk==3.6.6; python_version > "2.7" packaging==20.3 pathspec==0.7.0 pluggy==0.13.1 py==1.10.0 pydantic==1.6.2 pyparsing==2.4.6 pytest==5.3.5 pyyaml==5.4 redis==4.3.6 regex==2020.2.20 requests==2.23.0 six==1.14.0 starlette==0.25.0 toml==0.10.0 tornado==6.0.4 tqdm==4.50.0; python_version > "2.7" typed-ast==1.4.1 typing-extensions==3.7.4.1 urllib3==1.26.5 wcwidth==0.1.8 zipp==3.1.0; python_version < "3.8" slowapi-0.1.9/mkdocs.yml000066400000000000000000000002651456015001600152030ustar00rootroot00000000000000site_name: SlowApi Documentation markdown_extensions: - admonition - codehilite - mkautodoc extra_css: - css/custom.css nav: - SlowApi: index.md - examples.md - api.md slowapi-0.1.9/poetry.lock000066400000000000000000003372731456015001600154100ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "anyio" version = "3.6.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "dev" optional = false python-versions = ">=3.6.2" files = [ {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, ] [package.dependencies] idna = ">=2.8" sniffio = ">=1.1" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] trio = ["trio (>=0.16,<0.22)"] [[package]] name = "atomicwrites" version = "1.4.1" description = "Atomic file writes." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, ] [[package]] name = "attrs" version = "22.2.0" description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, ] [package.extras] cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] dev = ["attrs[docs,tests]"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] tests = ["attrs[tests-no-zope]", "zope.interface"] tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] [[package]] name = "black" version = "23.3.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] [[package]] name = "charset-normalizer" version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false python-versions = ">=3.7.0" files = [ {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, ] [[package]] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "coverage" version = "6.5.0" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] [package.extras] toml = ["tomli"] [[package]] name = "deprecated" version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] [package.dependencies] wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] [[package]] name = "fastapi" version = "0.89.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "fastapi-0.89.1-py3-none-any.whl", hash = "sha256:f9773ea22290635b2f48b4275b2bf69a8fa721fda2e38228bed47139839dc877"}, {file = "fastapi-0.89.1.tar.gz", hash = "sha256:15d9271ee52b572a015ca2ae5c72e1ce4241dd8532a534ad4f7ec70c376a580f"}, ] [package.dependencies] pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" starlette = "0.22.0" [package.extras] all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.8.0)"] test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.10.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.6.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] [[package]] name = "flake8" version = "4.0.1" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, ] [package.dependencies] importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.8.0,<2.9.0" pyflakes = ">=2.4.0,<2.5.0" [[package]] name = "ghp-import" version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." category = "dev" optional = false python-versions = "*" files = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] [package.dependencies] python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] [package.dependencies] typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "hiro" version = "0.5.1" description = "time manipulation utilities for python" category = "dev" optional = false python-versions = "*" files = [ {file = "hiro-0.5.1.tar.gz", hash = "sha256:d10e3b7f27b36673b4fa1283cd38d610326ba1ff1291260d0275152f15ae4bc7"}, ] [package.dependencies] mock = "*" six = ">=1.4.1" [[package]] name = "httpcore" version = "0.16.3" description = "A minimal low-level HTTP client." category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, ] [package.dependencies] anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" sniffio = ">=1.0.0,<2.0.0" [package.extras] http2 = ["h2 (>=3,<5)"] socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" version = "0.23.3" description = "The next generation HTTP client." category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, ] [package.dependencies] certifi = "*" httpcore = ">=0.15.0,<0.17.0" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false python-versions = ">=3.5" files = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] [[package]] name = "importlib-metadata" version = "4.2.0" description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, ] [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] [[package]] name = "importlib-resources" version = "5.12.0" description = "Read resources from Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "isort" version = "4.3.21" description = "A Python utility / library to sort Python imports." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, ] [package.extras] pipfile = ["pipreqs", "requirementslib"] pyproject = ["toml"] requirements = ["pip-api", "pipreqs"] xdg-home = ["appdirs (>=1.4.0)"] [[package]] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "limits" version = "3.3.1" description = "Rate limiting utilities" category = "main" optional = false python-versions = ">=3.7" files = [ {file = "limits-3.3.1-py3-none-any.whl", hash = "sha256:df8685b1aff349b5199628ecdf41a9f339a35233d8e4fcd9c3e10002e4419b45"}, {file = "limits-3.3.1.tar.gz", hash = "sha256:dfc59ed5b4847e33a33b88ec16033bed18ce444ce6a76287a4e054db9a683861"}, ] [package.dependencies] deprecated = ">=1.2" importlib-resources = ">=1.3" packaging = ">=21,<24" setuptools = "*" typing-extensions = "*" [package.extras] all = ["aetcd", "coredis (>=3.4.0,<5)", "emcache (>=0.6.1)", "emcache (>=1)", "etcd3", "motor (>=3,<4)", "pymemcache (>3,<5.0.0)", "pymongo (>4.1,<5)", "redis (>3,!=4.5.2,!=4.5.3,<5.0.0)", "redis (>=4.2.0,!=4.5.2,!=4.5.3)"] async-etcd = ["aetcd"] async-memcached = ["emcache (>=0.6.1)", "emcache (>=1)"] async-mongodb = ["motor (>=3,<4)"] async-redis = ["coredis (>=3.4.0,<5)"] etcd = ["etcd3"] memcached = ["pymemcache (>3,<5.0.0)"] mongodb = ["pymongo (>4.1,<5)"] redis = ["redis (>3,!=4.5.2,!=4.5.3,<5.0.0)"] rediscluster = ["redis (>=4.2.0,!=4.5.2,!=4.5.3)"] [[package]] name = "lxml" version = "4.9.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" files = [ {file = "lxml-4.9.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2"}, {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892"}, {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a"}, {file = "lxml-4.9.2-cp27-cp27m-win32.whl", hash = "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de"}, {file = "lxml-4.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3"}, {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50"}, {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975"}, {file = "lxml-4.9.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c"}, {file = "lxml-4.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a"}, {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4"}, {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4"}, {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7"}, {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184"}, {file = "lxml-4.9.2-cp310-cp310-win32.whl", hash = "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda"}, {file = "lxml-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab"}, {file = "lxml-4.9.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9"}, {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf"}, {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380"}, {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92"}, {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1"}, {file = "lxml-4.9.2-cp311-cp311-win32.whl", hash = "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33"}, {file = "lxml-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd"}, {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0"}, {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e"}, {file = "lxml-4.9.2-cp35-cp35m-win32.whl", hash = "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df"}, {file = "lxml-4.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5"}, {file = "lxml-4.9.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53"}, {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7"}, {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe"}, {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"}, {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1"}, {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e"}, {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74"}, {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38"}, {file = "lxml-4.9.2-cp36-cp36m-win32.whl", hash = "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5"}, {file = "lxml-4.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3"}, {file = "lxml-4.9.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03"}, {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941"}, {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726"}, {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b"}, {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894"}, {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45"}, {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e"}, {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b"}, {file = "lxml-4.9.2-cp37-cp37m-win32.whl", hash = "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe"}, {file = "lxml-4.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9"}, {file = "lxml-4.9.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8"}, {file = "lxml-4.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24"}, {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889"}, {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f"}, {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03"}, {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c"}, {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f"}, {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457"}, {file = "lxml-4.9.2-cp38-cp38-win32.whl", hash = "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b"}, {file = "lxml-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7"}, {file = "lxml-4.9.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1"}, {file = "lxml-4.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140"}, {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4"}, {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf"}, {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947"}, {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5"}, {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5"}, {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2"}, {file = "lxml-4.9.2-cp39-cp39-win32.whl", hash = "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1"}, {file = "lxml-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f"}, {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c"}, {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a"}, {file = "lxml-4.9.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419"}, {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05"}, {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f"}, {file = "lxml-4.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9"}, {file = "lxml-4.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5"}, {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746"}, {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7"}, {file = "lxml-4.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409"}, {file = "lxml-4.9.2.tar.gz", hash = "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=0.29.7)"] [[package]] name = "markdown" version = "3.3.4" description = "Python implementation of Markdown." category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, ] [package.dependencies] importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, ] [[package]] name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" category = "dev" optional = false python-versions = "*" files = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] [[package]] name = "mergedeep" version = "1.3.4" description = "A deep merge function for 🐍." category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] [[package]] name = "mkautodoc" version = "0.1.0" description = "AutoDoc for MarkDown" category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "mkautodoc-0.1.0.tar.gz", hash = "sha256:7c2595f40276b356e576ce7e343338f8b4fa1e02ea904edf33fadf82b68ca67c"}, ] [[package]] name = "mkdocs" version = "1.2.4" description = "Project documentation with Markdown." category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "mkdocs-1.2.4-py3-none-any.whl", hash = "sha256:f108e7ab5a7ed3e30826dbf82f37638f0d90d11161644616cc4f01a1e2ab3940"}, {file = "mkdocs-1.2.4.tar.gz", hash = "sha256:8e7970a26183487fe2a1041940c6fd03aa0dbe5549e50c3e7194f565cb3c678a"}, ] [package.dependencies] click = ">=3.3" ghp-import = ">=1.0" importlib-metadata = ">=3.10" Jinja2 = ">=2.10.1" Markdown = ">=3.2.1" mergedeep = ">=1.3.4" packaging = ">=20.5" PyYAML = ">=3.10" pyyaml-env-tag = ">=0.1" watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] [[package]] name = "mock" version = "4.0.3" description = "Rolling backport of unittest.mock for all Pythons" category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"}, {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"}, ] [package.extras] build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest (<5.4)", "pytest-cov"] [[package]] name = "mypy" version = "0.910" description = "Optional static typing for Python" category = "dev" optional = false python-versions = ">=3.5" files = [ {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, ] [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" toml = "*" typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} typing-extensions = ">=3.7.4" [package.extras] dmypy = ["psutil (>=4.0)"] python2 = ["typed-ast (>=1.4.0,<1.5.0)"] [[package]] name = "mypy-extensions" version = "0.4.4" description = "Experimental type system extensions for programs checked with the mypy typechecker." category = "dev" optional = false python-versions = ">=2.7" files = [ {file = "mypy_extensions-0.4.4.tar.gz", hash = "sha256:c8b707883a96efe9b4bb3aaf0dcc07e7e217d7d8368eec4db4049ee9e142f4fd"}, ] [[package]] name = "packaging" version = "23.0" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] [[package]] name = "pathspec" version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, ] [[package]] name = "platformdirs" version = "3.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, ] [package.dependencies] typing-extensions = {version = ">=4.5", markers = "python_version < \"3.8\""} [package.extras] docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] [[package]] name = "pycodestyle" version = "2.8.0" description = "Python style guide checker" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] [[package]] name = "pydantic" version = "1.10.7" description = "Data validation and settings management using python type hints" category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, ] [package.dependencies] typing-extensions = ">=4.2.0" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] [[package]] name = "pyflakes" version = "2.4.0" description = "passive checker of Python programs" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] [[package]] name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] [package.dependencies] six = ">=1.5" [[package]] name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] [[package]] name = "pyyaml-env-tag" version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " category = "dev" optional = false python-versions = ">=3.6" files = [ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, ] [package.dependencies] pyyaml = "*" [[package]] name = "redis" version = "3.5.3" description = "Python client for Redis key-value store" category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, ] [package.extras] hiredis = ["hiredis (>=0.1.3)"] [[package]] name = "requests" version = "2.28.2" description = "Python HTTP for Humans." category = "dev" optional = false python-versions = ">=3.7, <4" files = [ {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rfc3986" version = "1.5.0" description = "Validating URI References per RFC 3986" category = "dev" optional = false python-versions = "*" files = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] [package.dependencies] idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} [package.extras] idna2008 = ["idna"] [[package]] name = "setuptools" version = "65.7.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ {file = "setuptools-65.7.0-py3-none-any.whl", hash = "sha256:8ab4f1dbf2b4a65f7eec5ad0c620e84c34111a68d3349833494b9088212214dd"}, {file = "setuptools-65.7.0.tar.gz", hash = "sha256:4d3c92fac8f1118bb77a22181355e29c239cabfe2b9effdaa665c66b711136d7"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] [[package]] name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] [[package]] name = "starlette" version = "0.22.0" description = "The little ASGI library that shines." category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "starlette-0.22.0-py3-none-any.whl", hash = "sha256:b5eda991ad5f0ee5d8ce4c4540202a573bb6691ecd0c712262d0bc85cf8f2c50"}, {file = "starlette-0.22.0.tar.gz", hash = "sha256:b092cbc365bea34dd6840b42861bdabb2f507f8671e642e8272d2442e08ea4ff"}, ] [package.dependencies] anyio = ">=3.4.0,<5" typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] [[package]] name = "typed-ast" version = "1.4.3" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false python-versions = "*" files = [ {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] [[package]] name = "types-redis" version = "3.5.18" description = "Typing stubs for redis" category = "dev" optional = false python-versions = "*" files = [ {file = "types-redis-3.5.18.tar.gz", hash = "sha256:15482304e8848c63b383b938ffaba7ebe0b7f8f33381ecc450ee03935213e166"}, {file = "types_redis-3.5.18-py3-none-any.whl", hash = "sha256:5c55c4b9e8ebdc6d57d4e47900b77d99f19ca0a563264af3f701246ed0926335"}, ] [[package]] name = "typing-extensions" version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" files = [ {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, ] [[package]] name = "urllib3" version = "1.26.15" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "watchdog" version = "3.0.0" description = "Filesystem events monitoring" category = "dev" optional = false python-versions = ">=3.7" files = [ {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"}, {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, ] [package.extras] watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, ] [[package]] name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" files = [ {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] redis = ["redis"] [metadata] lock-version = "2.0" python-versions = ">=3.7,<4.0" content-hash = "b0fbb75051b47ba71537e15c61e7bbbccd8edf91d806dc2ce83be350ed711297" slowapi-0.1.9/pyproject.toml000066400000000000000000000022461456015001600161150ustar00rootroot00000000000000[tool.poetry] name = "slowapi" version = "0.1.9" description = "A rate limiting extension for Starlette and Fastapi" authors = ["Laurent Savaete "] license = "MIT" readme = "README.md" repository = "https://github.com/laurents/slowapi" homepage = "https://github.com/laurents/slowapi" documentation = "https://slowapi.readthedocs.io/en/latest/" include = ["slowapi/py.typed"] [tool.poetry.dependencies] python = ">=3.7,<4.0" limits = ">=2.3" redis = {version = "^3.4.1", optional = true} [tool.poetry.dev-dependencies] isort = "^4.3.21" mypy = "^0.910" black = "^23.0.0" fastapi = "^0.89.0" lxml = "^4.9.1" starlette = "^0.22.0" mock = "^4.0.1" hiro = "^0.5.1" requests = "^2.22.0" pytest = "~=6.2.5" mkdocs = "^1.2.3" mkautodoc = "^0.1.0" types-redis = "^3.5.6" coverage = "^6.3" flake8 = "^4.0.1" setuptools = "^65.5.0" httpx = "^0.23.3" [tool.black] line-length = 88 [tool.isort] multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true line_length = 88 [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.extras] redis = ["redis"] slowapi-0.1.9/renovate.json000066400000000000000000000001531456015001600157120ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" ] } slowapi-0.1.9/slowapi/000077500000000000000000000000001456015001600146535ustar00rootroot00000000000000slowapi-0.1.9/slowapi/__init__.py000066400000000000000000000001641456015001600167650ustar00rootroot00000000000000from .extension import Limiter, _rate_limit_exceeded_handler __all__ = ["Limiter", "_rate_limit_exceeded_handler"] slowapi-0.1.9/slowapi/errors.py000066400000000000000000000012211456015001600165350ustar00rootroot00000000000000""" errors and exceptions """ from starlette.exceptions import HTTPException from .wrappers import Limit class RateLimitExceeded(HTTPException): """ exception raised when a rate limit is hit. """ limit = None def __init__(self, limit: Limit) -> None: self.limit = limit if limit.error_message: description: str = ( limit.error_message if not callable(limit.error_message) else limit.error_message() ) else: description = str(limit.limit) super(RateLimitExceeded, self).__init__(status_code=429, detail=description) slowapi-0.1.9/slowapi/extension.py000066400000000000000000001044601456015001600172460ustar00rootroot00000000000000""" The starlette extension to rate-limit requests """ import asyncio import functools import inspect import itertools import logging import os import time from datetime import datetime from email.utils import formatdate, parsedate_to_datetime from functools import wraps from typing import ( Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union, ) from limits import RateLimitItem # type: ignore from limits.errors import ConfigurationError # type: ignore from limits.storage import MemoryStorage, storage_from_string # type: ignore from limits.strategies import STRATEGIES, RateLimiter # type: ignore from starlette.config import Config from starlette.datastructures import MutableHeaders from starlette.requests import Request from starlette.responses import JSONResponse, Response from typing_extensions import Literal from .errors import RateLimitExceeded from .wrappers import Limit, LimitGroup # used to annotate get_app_config method T = TypeVar("T") # Define an alias for the most commonly used type StrOrCallableStr = Union[str, Callable[..., str]] class C: ENABLED = "RATELIMIT_ENABLED" HEADERS_ENABLED = "RATELIMIT_HEADERS_ENABLED" STORAGE_URL = "RATELIMIT_STORAGE_URL" STORAGE_OPTIONS = "RATELIMIT_STORAGE_OPTIONS" STRATEGY = "RATELIMIT_STRATEGY" GLOBAL_LIMITS = "RATELIMIT_GLOBAL" DEFAULT_LIMITS = "RATELIMIT_DEFAULT" APPLICATION_LIMITS = "RATELIMIT_APPLICATION" HEADER_LIMIT = "RATELIMIT_HEADER_LIMIT" HEADER_REMAINING = "RATELIMIT_HEADER_REMAINING" HEADER_RESET = "RATELIMIT_HEADER_RESET" SWALLOW_ERRORS = "RATELIMIT_SWALLOW_ERRORS" IN_MEMORY_FALLBACK = "RATELIMIT_IN_MEMORY_FALLBACK" IN_MEMORY_FALLBACK_ENABLED = "RATELIMIT_IN_MEMORY_FALLBACK_ENABLED" HEADER_RETRY_AFTER = "RATELIMIT_HEADER_RETRY_AFTER" HEADER_RETRY_AFTER_VALUE = "RATELIMIT_HEADER_RETRY_AFTER_VALUE" KEY_PREFIX = "RATELIMIT_KEY_PREFIX" class HEADERS: RESET = 1 REMAINING = 2 LIMIT = 3 RETRY_AFTER = 4 MAX_BACKEND_CHECKS = 5 def _rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded) -> Response: """ Build a simple JSON response that includes the details of the rate limit that was hit. If no limit is hit, the countdown is added to headers. """ response = JSONResponse( {"error": f"Rate limit exceeded: {exc.detail}"}, status_code=429 ) response = request.app.state.limiter._inject_headers( response, request.state.view_rate_limit ) return response class Limiter: """ Initializes the slowapi rate limiter. ** parameter ** * **app**: `Starlette/FastAPI` instance to initialize the extension with. * **default_limits**: a variable list of strings or callables returning strings denoting global limits to apply to all routes. `ratelimit-string` for more details. * **application_limits**: a variable list of strings or callables returning strings for limits that are applied to the entire application (i.e a shared limit for all routes) * **key_func**: a callable that returns the domain to rate limit by. * **headers_enabled**: whether ``X-RateLimit`` response headers are written. * **strategy:** the strategy to use. refer to `ratelimit-strategy` * **storage_uri**: the storage location. refer to `ratelimit-conf` * **storage_options**: kwargs to pass to the storage implementation upon instantiation. * **auto_check**: whether to automatically check the rate limit in the before_request chain of the application. default ``True`` * **swallow_errors**: whether to swallow errors when hitting a rate limit. An exception will still be logged. default ``False`` * **in_memory_fallback**: a variable list of strings or callables returning strings denoting fallback limits to apply when the storage is down. * **in_memory_fallback_enabled**: simply falls back to in memory storage when the main storage is down and inherits the original limits. * **key_prefix**: prefix prepended to rate limiter keys. * **enabled**: set to False to deactivate the limiter (default: True) * **config_filename**: name of the config file for Starlette from which to load settings for the rate limiter. Defaults to ".env". * **key_style**: set to "url" to use the url, "endpoint" to use the view_func """ def __init__( self, # app: Starlette = None, key_func: Callable[..., str], default_limits: List[StrOrCallableStr] = [], application_limits: List[StrOrCallableStr] = [], headers_enabled: bool = False, strategy: Optional[str] = None, storage_uri: Optional[str] = None, storage_options: Dict[str, str] = {}, auto_check: bool = True, swallow_errors: bool = False, in_memory_fallback: List[StrOrCallableStr] = [], in_memory_fallback_enabled: bool = False, retry_after: Optional[str] = None, key_prefix: str = "", enabled: bool = True, config_filename: Optional[str] = None, key_style: Literal["endpoint", "url"] = "url", ) -> None: """ Configure the rate limiter at app level """ # assert app is not None, "Passing the app instance to the limiter is required" # self.app = app # app.state.limiter = self self.logger = logging.getLogger("slowapi") dotenv_file_exists = os.path.isfile(".env") self.app_config = Config( ".env" if dotenv_file_exists and config_filename is None else config_filename ) self.enabled = enabled self._default_limits = [] self._application_limits = [] self._in_memory_fallback: List[LimitGroup] = [] self._in_memory_fallback_enabled = ( in_memory_fallback_enabled or len(in_memory_fallback) > 0 ) self._exempt_routes: Set[str] = set() self._request_filters: List[Callable[..., bool]] = [] self._headers_enabled = headers_enabled self._header_mapping: Dict[int, str] = {} self._retry_after: Optional[str] = retry_after self._strategy = strategy self._storage_uri = storage_uri self._storage_options = storage_options self._auto_check = auto_check self._swallow_errors = swallow_errors self._key_func = key_func self._key_prefix = key_prefix self._key_style = key_style for limit in set(default_limits): self._default_limits.extend( [ LimitGroup( limit, self._key_func, None, False, None, None, None, 1, False ) ] ) for limit in application_limits: self._application_limits.extend( [ LimitGroup( limit, self._key_func, "global", False, None, None, None, 1, False, ) ] ) for limit in in_memory_fallback: self._in_memory_fallback.extend( [ LimitGroup( limit, self._key_func, None, False, None, None, None, 1, False ) ] ) self._route_limits: Dict[str, List[Limit]] = {} self._dynamic_route_limits: Dict[str, List[LimitGroup]] = {} # a flag to note if the storage backend is dead (not available) self._storage_dead: bool = False self._fallback_limiter = None self.__check_backend_count = 0 self.__last_check_backend = time.time() self.__marked_for_limiting: Dict[str, List[Callable]] = {} class BlackHoleHandler(logging.StreamHandler): def emit(*_): return self.logger.addHandler(BlackHoleHandler()) self.enabled = self.get_app_config(C.ENABLED, self.enabled) self._swallow_errors = self.get_app_config( C.SWALLOW_ERRORS, self._swallow_errors ) self._headers_enabled = self._headers_enabled or self.get_app_config( C.HEADERS_ENABLED, False ) self._storage_options.update(self.get_app_config(C.STORAGE_OPTIONS, {})) self._storage = storage_from_string( self._storage_uri or self.get_app_config(C.STORAGE_URL, "memory://"), **self._storage_options, ) strategy = self._strategy or self.get_app_config(C.STRATEGY, "fixed-window") if strategy not in STRATEGIES: raise ConfigurationError("Invalid rate limiting strategy %s" % strategy) self._limiter: RateLimiter = STRATEGIES[strategy](self._storage) self._header_mapping.update( { HEADERS.RESET: self._header_mapping.get( HEADERS.RESET, self.get_app_config(C.HEADER_RESET, "X-RateLimit-Reset"), ), HEADERS.REMAINING: self._header_mapping.get( HEADERS.REMAINING, self.get_app_config(C.HEADER_REMAINING, "X-RateLimit-Remaining"), ), HEADERS.LIMIT: self._header_mapping.get( HEADERS.LIMIT, self.get_app_config(C.HEADER_LIMIT, "X-RateLimit-Limit"), ), HEADERS.RETRY_AFTER: self._header_mapping.get( HEADERS.RETRY_AFTER, self.get_app_config(C.HEADER_RETRY_AFTER, "Retry-After"), ), } ) self._retry_after = self._retry_after or self.get_app_config( C.HEADER_RETRY_AFTER_VALUE ) self._key_prefix = self._key_prefix or self.get_app_config(C.KEY_PREFIX) app_limits: Optional[StrOrCallableStr] = self.get_app_config( C.APPLICATION_LIMITS, None ) if not self._application_limits and app_limits: self._application_limits = [ LimitGroup( app_limits, self._key_func, "global", False, None, None, None, 1, False, ) ] conf_limits: Optional[StrOrCallableStr] = self.get_app_config( C.DEFAULT_LIMITS, None ) if not self._default_limits and conf_limits: self._default_limits = [ LimitGroup( conf_limits, self._key_func, None, False, None, None, None, 1, False ) ] fallback_enabled = self.get_app_config(C.IN_MEMORY_FALLBACK_ENABLED, False) fallback_limits: Optional[StrOrCallableStr] = self.get_app_config( C.IN_MEMORY_FALLBACK, None ) if not self._in_memory_fallback and fallback_limits: self._in_memory_fallback = [ LimitGroup( fallback_limits, self._key_func, None, False, None, None, None, 1, False, ) ] if not self._in_memory_fallback_enabled: self._in_memory_fallback_enabled = ( fallback_enabled or len(self._in_memory_fallback) > 0 ) if self._in_memory_fallback_enabled: self._fallback_storage = MemoryStorage() self._fallback_limiter = STRATEGIES[strategy](self._fallback_storage) def slowapi_startup(self) -> None: """ Starlette startup event handler that links the app with the Limiter instance. """ app.state.limiter = self # type: ignore app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore def get_app_config(self, key: str, default_value: T = None) -> T: """ Place holder until we find a better way to load config from app """ return ( self.app_config(key, default=default_value, cast=type(default_value)) if default_value else self.app_config(key, default=default_value) ) def __should_check_backend(self) -> bool: if self.__check_backend_count > MAX_BACKEND_CHECKS: self.__check_backend_count = 0 if time.time() - self.__last_check_backend > pow(2, self.__check_backend_count): self.__last_check_backend = time.time() self.__check_backend_count += 1 return True return False def reset(self) -> None: """ resets the storage if it supports being reset """ try: self._storage.reset() self.logger.info("Storage has been reset and all limits cleared") except NotImplementedError: self.logger.warning("This storage type does not support being reset") @property def limiter(self) -> RateLimiter: """ The backend that keeps track of consumption of endpoints vs limits """ if self._storage_dead and self._in_memory_fallback_enabled: assert ( self._fallback_limiter ), "Fallback limiter is needed when in memory fallback is enabled" return self._fallback_limiter else: return self._limiter def _inject_headers( self, response: Response, current_limit: Tuple[RateLimitItem, List[str]] ) -> Response: if self.enabled and self._headers_enabled and current_limit is not None: if not isinstance(response, Response): raise Exception( "parameter `response` must be an instance of starlette.responses.Response" ) try: window_stats: Tuple[int, int] = self.limiter.get_window_stats( current_limit[0], *current_limit[1] ) reset_in = 1 + window_stats[0] response.headers.append( self._header_mapping[HEADERS.LIMIT], str(current_limit[0].amount) ) response.headers.append( self._header_mapping[HEADERS.REMAINING], str(window_stats[1]) ) response.headers.append( self._header_mapping[HEADERS.RESET], str(reset_in) ) # response may have an existing retry after existing_retry_after_header = response.headers.get("Retry-After") if existing_retry_after_header is not None: reset_in = max( self._determine_retry_time(existing_retry_after_header), reset_in, ) response.headers[self._header_mapping[HEADERS.RETRY_AFTER]] = ( formatdate(reset_in) if self._retry_after == "http-date" else str(int(reset_in - time.time())) ) except: if self._in_memory_fallback and not self._storage_dead: self.logger.warning( "Rate limit storage unreachable - falling back to" " in-memory storage" ) self._storage_dead = True response = self._inject_headers(response, current_limit) if self._swallow_errors: self.logger.exception( "Failed to update rate limit headers. Swallowing error" ) else: raise return response def _inject_asgi_headers( self, headers: MutableHeaders, current_limit: Tuple[RateLimitItem, List[str]] ) -> MutableHeaders: """ Injects 'X-RateLimit-Reset', 'X-RateLimit-Remaining', 'X-RateLimit-Limit' and 'Retry-After' headers into :headers parameter if needed. Basically the same as _inject_headers, but without access to the Response object. -> supports ASGI Middlewares. """ if self.enabled and self._headers_enabled and current_limit is not None: try: window_stats: Tuple[int, int] = self.limiter.get_window_stats( current_limit[0], *current_limit[1] ) reset_in = 1 + window_stats[0] headers[self._header_mapping[HEADERS.LIMIT]] = str( current_limit[0].amount ) headers[self._header_mapping[HEADERS.REMAINING]] = str(window_stats[1]) headers[self._header_mapping[HEADERS.RESET]] = str(reset_in) # response may have an existing retry after existing_retry_after_header = headers.get("Retry-After") if existing_retry_after_header is not None: reset_in = max( self._determine_retry_time(existing_retry_after_header), reset_in, ) headers[self._header_mapping[HEADERS.RETRY_AFTER]] = ( formatdate(reset_in) if self._retry_after == "http-date" else str(int(reset_in - time.time())) ) except Exception: if self._in_memory_fallback and not self._storage_dead: self.logger.warning( "Rate limit storage unreachable - falling back to" " in-memory storage" ) self._storage_dead = True headers = self._inject_asgi_headers(headers, current_limit) if self._swallow_errors: self.logger.exception( "Failed to update rate limit headers. Swallowing error" ) else: raise return headers def __evaluate_limits( self, request: Request, endpoint: str, limits: List[Limit] ) -> None: failed_limit = None limit_for_header = None for lim in limits: limit_scope = lim.scope or endpoint if lim.is_exempt: continue if lim.methods is not None and request.method.lower() not in lim.methods: continue if lim.per_method: limit_scope += ":%s" % request.method if "request" in inspect.signature(lim.key_func).parameters.keys(): limit_key = lim.key_func(request) else: limit_key = lim.key_func() args = [limit_key, limit_scope] if all(args): if self._key_prefix: args = [self._key_prefix] + args if not limit_for_header or lim.limit < limit_for_header[0]: limit_for_header = (lim.limit, args) cost = lim.cost(request) if callable(lim.cost) else lim.cost if not self.limiter.hit(lim.limit, *args, cost=cost): self.logger.warning( "ratelimit %s (%s) exceeded at endpoint: %s", lim.limit, limit_key, limit_scope, ) failed_limit = lim limit_for_header = (lim.limit, args) break else: self.logger.error( "Skipping limit: %s. Empty value found in parameters.", lim.limit ) continue # keep track of which limit was hit, to be picked up for the response header request.state.view_rate_limit = limit_for_header if failed_limit: raise RateLimitExceeded(failed_limit) def _determine_retry_time(self, retry_header_value) -> int: try: retry_after_date: Optional[datetime] = parsedate_to_datetime( retry_header_value ) except (TypeError, ValueError): retry_after_date = None if retry_after_date is not None: return int(time.mktime(retry_after_date.timetuple())) try: retry_after_int: int = int(retry_header_value) except TypeError: raise ValueError( "Retry-After Header does not meet RFC2616 - value is not of http-date or int type." ) return int(time.time() + retry_after_int) def _check_request_limit( self, request: Request, endpoint_func: Optional[Callable[..., Any]], in_middleware: bool = True, ) -> None: """ Determine if the request is within limits """ endpoint_url = request["path"] or "" view_func = endpoint_func endpoint_func_name = ( f"{view_func.__module__}.{view_func.__name__}" if view_func else "" ) _endpoint_key = endpoint_url if self._key_style == "url" else endpoint_func_name # cases where we don't need to check the limits if ( not _endpoint_key or not self.enabled # or we are sending a static file # or view_func == current_app.send_static_file or endpoint_func_name in self._exempt_routes or any(fn() for fn in self._request_filters) ): return limits: List[Limit] = [] dynamic_limits: List[Limit] = [] if not in_middleware: limits = ( self._route_limits[endpoint_func_name] if endpoint_func_name in self._route_limits else [] ) dynamic_limits = [] if endpoint_func_name in self._dynamic_route_limits: for lim in self._dynamic_route_limits[endpoint_func_name]: try: dynamic_limits.extend(list(lim.with_request(request))) except ValueError as e: self.logger.error( "failed to load ratelimit for view function %s (%s)", endpoint_func_name, e, ) try: all_limits: List[Limit] = [] if self._storage_dead and self._fallback_limiter: if in_middleware and endpoint_func_name in self.__marked_for_limiting: pass else: if self.__should_check_backend() and self._storage.check(): self.logger.info("Rate limit storage recovered") self._storage_dead = False self.__check_backend_count = 0 else: all_limits = list(itertools.chain(*self._in_memory_fallback)) if not all_limits: route_limits: List[Limit] = limits + dynamic_limits all_limits = ( list(itertools.chain(*self._application_limits)) if in_middleware else [] ) all_limits += route_limits combined_defaults = all( not limit.override_defaults for limit in route_limits ) if ( not route_limits and not ( in_middleware and endpoint_func_name in self.__marked_for_limiting ) or combined_defaults ): all_limits += list(itertools.chain(*self._default_limits)) # actually check the limits, so far we've only computed the list of limits to check self.__evaluate_limits(request, _endpoint_key, all_limits) except Exception as e: # no qa if isinstance(e, RateLimitExceeded): raise if self._in_memory_fallback_enabled and not self._storage_dead: self.logger.warn( "Rate limit storage unreachable - falling back to" " in-memory storage" ) self._storage_dead = True self._check_request_limit(request, endpoint_func, in_middleware) else: if self._swallow_errors: self.logger.exception("Failed to rate limit. Swallowing error") else: raise def __limit_decorator( self, limit_value: StrOrCallableStr, key_func: Optional[Callable[..., str]] = None, shared: bool = False, scope: Optional[StrOrCallableStr] = None, per_method: bool = False, methods: Optional[List[str]] = None, error_message: Optional[str] = None, exempt_when: Optional[Callable[..., bool]] = None, cost: Union[int, Callable[..., int]] = 1, override_defaults: bool = True, ) -> Callable[..., Any]: _scope = scope if shared else None def decorator(func: Callable[..., Response]): keyfunc = key_func or self._key_func name = f"{func.__module__}.{func.__name__}" dynamic_limit = None static_limits: List[Limit] = [] if callable(limit_value): dynamic_limit = LimitGroup( limit_value, keyfunc, _scope, per_method, methods, error_message, exempt_when, cost, override_defaults, ) else: try: static_limits = list( LimitGroup( limit_value, keyfunc, _scope, per_method, methods, error_message, exempt_when, cost, override_defaults, ) ) except ValueError as e: self.logger.error( "Failed to configure throttling for %s (%s)", name, e, ) self.__marked_for_limiting.setdefault(name, []).append(func) if dynamic_limit: self._dynamic_route_limits.setdefault(name, []).append(dynamic_limit) else: self._route_limits.setdefault(name, []).extend(static_limits) connection_type: Optional[str] = None sig = inspect.signature(func) for idx, parameter in enumerate(sig.parameters.values()): if parameter.name == "request" or parameter.name == "websocket": connection_type = parameter.name break else: raise Exception( f'No "request" or "websocket" argument on function "{func}"' ) if asyncio.iscoroutinefunction(func): # Handle async request/response functions. @functools.wraps(func) async def async_wrapper(*args: Any, **kwargs: Any) -> Response: # get the request object from the decorated endpoint function if self.enabled: request = kwargs.get("request", args[idx] if args else None) if not isinstance(request, Request): raise Exception( "parameter `request` must be an instance of starlette.requests.Request" ) if self._auto_check and not getattr( request.state, "_rate_limiting_complete", False ): self._check_request_limit(request, func, False) request.state._rate_limiting_complete = True response = await func(*args, **kwargs) # type: ignore if self.enabled: if not isinstance(response, Response): # get the response object from the decorated endpoint function self._inject_headers( kwargs.get("response"), request.state.view_rate_limit # type: ignore ) else: self._inject_headers( response, request.state.view_rate_limit ) return response return async_wrapper else: # Handle sync request/response functions. @functools.wraps(func) def sync_wrapper(*args: Any, **kwargs: Any) -> Response: # get the request object from the decorated endpoint function if self.enabled: request = kwargs.get("request", args[idx] if args else None) if not isinstance(request, Request): raise Exception( "parameter `request` must be an instance of starlette.requests.Request" ) if self._auto_check and not getattr( request.state, "_rate_limiting_complete", False ): self._check_request_limit(request, func, False) request.state._rate_limiting_complete = True response = func(*args, **kwargs) if self.enabled: if not isinstance(response, Response): # get the response object from the decorated endpoint function self._inject_headers( kwargs.get("response"), request.state.view_rate_limit # type: ignore ) else: self._inject_headers( response, request.state.view_rate_limit ) return response return sync_wrapper return decorator def limit( self, limit_value: StrOrCallableStr, key_func: Optional[Callable[..., str]] = None, per_method: bool = False, methods: Optional[List[str]] = None, error_message: Optional[str] = None, exempt_when: Optional[Callable[..., bool]] = None, cost: Union[int, Callable[..., int]] = 1, override_defaults: bool = True, ) -> Callable: """ Decorator to be used for rate limiting individual routes. * **limit_value**: rate limit string or a callable that returns a string. :ref:`ratelimit-string` for more details. * **key_func**: function/lambda to extract the unique identifier for the rate limit. defaults to remote address of the request. * **per_method**: whether the limit is sub categorized into the http method of the request. * **methods**: if specified, only the methods in this list will be rate limited (default: None). * **error_message**: string (or callable that returns one) to override the error message used in the response. * **exempt_when**: function returning a boolean indicating whether to exempt the route from the limit * **cost**: integer (or callable that returns one) which is the cost of a hit * **override_defaults**: whether to override the default limits (default: True) """ return self.__limit_decorator( limit_value, key_func, per_method=per_method, methods=methods, error_message=error_message, exempt_when=exempt_when, cost=cost, override_defaults=override_defaults, ) def shared_limit( self, limit_value: StrOrCallableStr, scope: StrOrCallableStr, key_func: Optional[Callable[..., str]] = None, error_message: Optional[str] = None, exempt_when: Optional[Callable[..., bool]] = None, cost: Union[int, Callable[..., int]] = 1, override_defaults: bool = True, ) -> Callable: """ Decorator to be applied to multiple routes sharing the same rate limit. * **limit_value**: rate limit string or a callable that returns a string. :ref:`ratelimit-string` for more details. * **scope**: a string or callable that returns a string for defining the rate limiting scope. * **key_func**: function/lambda to extract the unique identifier for the rate limit. defaults to remote address of the request. * **per_method**: whether the limit is sub categorized into the http method of the request. * **methods**: if specified, only the methods in this list will be rate limited (default: None). * **error_message**: string (or callable that returns one) to override the error message used in the response. * **exempt_when**: function returning a boolean indicating whether to exempt the route from the limit * **cost**: integer (or callable that returns one) which is the cost of a hit * **override_defaults**: whether to override the default limits (default: True) """ return self.__limit_decorator( limit_value, key_func, True, scope, error_message=error_message, exempt_when=exempt_when, cost=cost, override_defaults=override_defaults, ) def exempt(self, obj): """ Decorator to mark a view as exempt from rate limits. """ name = "%s.%s" % (obj.__module__, obj.__name__) self._exempt_routes.add(name) if asyncio.iscoroutinefunction(obj): @wraps(obj) async def __async_inner(*a, **k): return await obj(*a, **k) return __async_inner else: @wraps(obj) def __inner(*a, **k): return obj(*a, **k) return __inner slowapi-0.1.9/slowapi/middleware.py000066400000000000000000000163221456015001600173460ustar00rootroot00000000000000import inspect from typing import Callable, Iterable, Optional, Tuple from starlette.applications import Starlette from starlette.datastructures import MutableHeaders from starlette.middleware.base import ( BaseHTTPMiddleware, RequestResponseEndpoint, ) from starlette.requests import Request from starlette.responses import Response from starlette.routing import BaseRoute, Match from starlette.types import ASGIApp, Message, Scope, Receive, Send from slowapi import Limiter, _rate_limit_exceeded_handler def _find_route_handler( routes: Iterable[BaseRoute], scope: Scope ) -> Optional[Callable]: handler = None for route in routes: match, _ = route.matches(scope) if match == Match.FULL and hasattr(route, "endpoint"): handler = route.endpoint # type: ignore return handler def _get_route_name(handler: Callable): return f"{handler.__module__}.{handler.__name__}" def _check_limits( limiter: Limiter, request: Request, handler: Optional[Callable], app: Starlette ) -> Tuple[Optional[Callable], bool, Optional[Exception]]: """ Utils to check (if needed) current requests limit. It returns a tuple of size 3: 1. The exception handler to run, if needed 2. a bool, True if we need to inject some headers, False otherwise 3. the exception that happened, if any """ if limiter._auto_check and not getattr( request.state, "_rate_limiting_complete", False ): try: limiter._check_request_limit(request, handler, True) except Exception as e: # handle the exception since the global exception handler won't pick it up if we call_next exception_handler = app.exception_handlers.get( type(e), _rate_limit_exceeded_handler ) return exception_handler, False, e return None, True, None return None, False, None def sync_check_limits( limiter: Limiter, request: Request, handler: Optional[Callable], app: Starlette ) -> Tuple[Optional[Response], bool]: """ Returns a `Response` object if an error occurred, as well as a boolean to know whether we should inject headers or not. Used in our WSGI middleware, it only supports synchronous exception_handler. This will fallback on _rate_limit_exceeded_handler otherwise. """ exception_handler, _bool, exc = _check_limits(limiter, request, handler, app) if not exception_handler or not exc: return None, _bool # cannot execute asynchronous code in a synchronous middleware, # -> fallback on default exception handler if inspect.iscoroutinefunction(exception_handler): exception_handler = _rate_limit_exceeded_handler return exception_handler(request, exc), _bool # type: ignore async def async_check_limits( limiter: Limiter, request: Request, handler: Optional[Callable], app: Starlette ) -> Tuple[Optional[Response], bool]: """ Returns a `Response` object if an error occurred, as well as a boolean to know whether we should inject headers or not. Used in our ASGI middleware, this support both synchronous or asynchronous exception handlers. """ exception_handler, _bool, exc = _check_limits(limiter, request, handler, app) if not exception_handler: return None, _bool if inspect.iscoroutinefunction(exception_handler): return await exception_handler(request, exc), _bool else: return exception_handler(request, exc), _bool def _should_exempt(limiter: Limiter, handler: Optional[Callable]) -> bool: # if we can't find the route handler if handler is None: return True name = _get_route_name(handler) # if exempt no need to check if name in limiter._exempt_routes: return True # there is a decorator for this route we let the decorator handle it if name in limiter._route_limits: return True return False class SlowAPIMiddleware(BaseHTTPMiddleware): async def dispatch( self, request: Request, call_next: RequestResponseEndpoint ) -> Response: app: Starlette = request.app limiter: Limiter = app.state.limiter if not limiter.enabled: return await call_next(request) handler = _find_route_handler(app.routes, request.scope) if _should_exempt(limiter, handler): return await call_next(request) error_response, should_inject_headers = sync_check_limits( limiter, request, handler, app ) if error_response is not None: return error_response response = await call_next(request) if should_inject_headers: response = limiter._inject_headers(response, request.state.view_rate_limit) return response class SlowAPIASGIMiddleware: def __init__(self, app: ASGIApp) -> None: self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] != "http": return await self.app(scope, receive, send) await _ASGIMiddlewareResponder(self.app)(scope, receive, send) class _ASGIMiddlewareResponder: def __init__(self, app: ASGIApp) -> None: self.app = app self.error_response: Optional[Response] = None self.initial_message: Message = {} self.inject_headers = False async def send_wrapper(self, message: Message) -> None: if message["type"] == "http.response.start": # do not send the http.response.start message now, so that we can edit the headers # before sending it, based on what happens in the http.response.body message. self.initial_message = message elif message["type"] == "http.response.body": if self.error_response: self.initial_message["status"] = self.error_response.status_code if self.inject_headers: headers = MutableHeaders(raw=self.initial_message["headers"]) headers = self.limiter._inject_asgi_headers( headers, self.request.state.view_rate_limit ) # send the http.response.start message just before the http.response.body one, # now that the headers are updated await self.send(self.initial_message) await self.send(message) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: self.send = send _app: Starlette = scope["app"] limiter: Limiter = _app.state.limiter if not limiter.enabled: return await self.app(scope, receive, self.send) handler = _find_route_handler(_app.routes, scope) request = Request(scope, receive=receive, send=self.send) if _should_exempt(limiter, handler): return await self.app(scope, receive, self.send) error_response, should_inject_headers = await async_check_limits( limiter, request, handler, _app ) if error_response is not None: return await error_response(scope, receive, self.send_wrapper) if should_inject_headers: self.inject_headers = True self.limiter = limiter self.request = request return await self.app(scope, receive, self.send_wrapper) slowapi-0.1.9/slowapi/py.typed000066400000000000000000000000001456015001600163400ustar00rootroot00000000000000slowapi-0.1.9/slowapi/util.py000066400000000000000000000015121456015001600162010ustar00rootroot00000000000000from starlette.requests import Request def get_ipaddr(request: Request) -> str: """ Returns the ip address for the current request (or 127.0.0.1 if none found) based on the X-Forwarded-For headers. Note that a more robust method for determining IP address of the client is provided by uvicorn's ProxyHeadersMiddleware. """ if "X_FORWARDED_FOR" in request.headers: return request.headers["X_FORWARDED_FOR"] else: if not request.client or not request.client.host: return "127.0.0.1" return request.client.host def get_remote_address(request: Request) -> str: """ Returns the ip address for the current request (or 127.0.0.1 if none found) """ if not request.client or not request.client.host: return "127.0.0.1" return request.client.host slowapi-0.1.9/slowapi/wrappers.py000066400000000000000000000074361456015001600171020ustar00rootroot00000000000000import inspect from typing import Callable, Iterator, List, Optional, Union from limits import RateLimitItem, parse_many # type: ignore class Limit(object): """ simple wrapper to encapsulate limits and their context """ def __init__( self, limit: RateLimitItem, key_func: Callable[..., str], scope: Optional[Union[str, Callable[..., str]]], per_method: bool, methods: Optional[List[str]], error_message: Optional[Union[str, Callable[..., str]]], exempt_when: Optional[Callable[..., bool]], cost: Union[int, Callable[..., int]], override_defaults: bool, ) -> None: self.limit = limit self.key_func = key_func self.__scope = scope self.per_method = per_method self.methods = methods self.error_message = error_message self.exempt_when = exempt_when self.cost = cost self.override_defaults = override_defaults @property def is_exempt(self) -> bool: """ Check if the limit is exempt. Return True to exempt the route from the limit. """ return self.exempt_when() if self.exempt_when is not None else False @property def scope(self) -> str: # flack.request.endpoint is the name of the function for the endpoint # FIXME: how to get the request here? if self.__scope is None: return "" else: return ( self.__scope(request.endpoint) # type: ignore if callable(self.__scope) else self.__scope ) class LimitGroup(object): """ represents a group of related limits either from a string or a callable that returns one """ def __init__( self, limit_provider: Union[str, Callable[..., str]], key_function: Callable[..., str], scope: Optional[Union[str, Callable[..., str]]], per_method: bool, methods: Optional[List[str]], error_message: Optional[Union[str, Callable[..., str]]], exempt_when: Optional[Callable[..., bool]], cost: Union[int, Callable[..., int]], override_defaults: bool, ): self.__limit_provider = limit_provider self.__scope = scope self.key_function = key_function self.per_method = per_method self.methods = methods and [m.lower() for m in methods] or methods self.error_message = error_message self.exempt_when = exempt_when self.cost = cost self.override_defaults = override_defaults self.request = None def __iter__(self) -> Iterator[Limit]: if callable(self.__limit_provider): if "key" in inspect.signature(self.__limit_provider).parameters.keys(): assert ( "request" in inspect.signature(self.key_function).parameters.keys() ), f"Limit provider function {self.key_function.__name__} needs a `request` argument" if self.request is None: raise Exception("`request` object can't be None") limit_raw = self.__limit_provider(self.key_function(self.request)) else: limit_raw = self.__limit_provider() else: limit_raw = self.__limit_provider limit_items: List[RateLimitItem] = parse_many(limit_raw) for limit in limit_items: yield Limit( limit, self.key_function, self.__scope, self.per_method, self.methods, self.error_message, self.exempt_when, self.cost, self.override_defaults, ) def with_request(self, request): self.request = request return self slowapi-0.1.9/tests/000077500000000000000000000000001456015001600143375ustar00rootroot00000000000000slowapi-0.1.9/tests/__init__.py000066400000000000000000000046051456015001600164550ustar00rootroot00000000000000import asyncio import logging import pytest from fastapi import FastAPI from mock import mock # type: ignore from starlette.applications import Starlette from starlette.requests import Request from slowapi.errors import RateLimitExceeded from slowapi.extension import Limiter, _rate_limit_exceeded_handler from slowapi.middleware import SlowAPIMiddleware, SlowAPIASGIMiddleware from slowapi.util import get_remote_address async def _async_rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded): await asyncio.sleep(0) return _rate_limit_exceeded_handler(request, exc) class TestSlowapi: @pytest.fixture( params=[ (SlowAPIMiddleware, _rate_limit_exceeded_handler), (SlowAPIASGIMiddleware, _rate_limit_exceeded_handler), (SlowAPIASGIMiddleware, _async_rate_limit_exceeded_handler), ] ) def build_starlette_app(self, request): def _factory(config={}, **limiter_args): middleware, exception_handler = request.param limiter_args.setdefault("key_func", get_remote_address) limiter = Limiter(**limiter_args) app = Starlette(debug=True) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, exception_handler) app.add_middleware(middleware) mock_handler = mock.Mock() mock_handler.level = logging.INFO limiter.logger.addHandler(mock_handler) return app, limiter return _factory @pytest.fixture( params=[ (SlowAPIMiddleware, _rate_limit_exceeded_handler), (SlowAPIASGIMiddleware, _rate_limit_exceeded_handler), (SlowAPIASGIMiddleware, _async_rate_limit_exceeded_handler), ] ) def build_fastapi_app(self, request): def _factory(config={}, **limiter_args): middleware, exception_handler = request.param limiter_args.setdefault("key_func", get_remote_address) limiter = Limiter(**limiter_args) app = FastAPI() app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, exception_handler) app.add_middleware(middleware) mock_handler = mock.Mock() mock_handler.level = logging.INFO limiter.logger.addHandler(mock_handler) return app, limiter return _factory slowapi-0.1.9/tests/test_base.py000066400000000000000000000000641456015001600166620ustar00rootroot00000000000000def test_import(): import slowapi # noqa: F401 slowapi-0.1.9/tests/test_fastapi_extension.py000066400000000000000000000327711456015001600215050ustar00rootroot00000000000000import hiro # type: ignore import pytest # type: ignore from starlette.requests import Request from starlette.responses import PlainTextResponse, Response from starlette.testclient import TestClient from slowapi.util import get_ipaddr from tests import TestSlowapi class TestDecorators(TestSlowapi): def test_single_decorator(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr) @app.get("/t1") @limiter.limit("5/minute") async def t1(request: Request): return PlainTextResponse("test") client = TestClient(app) for i in range(0, 10): response = client.get("/t1") assert response.status_code == 200 if i < 5 else 429 def test_single_decorator_with_headers(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr, headers_enabled=True) @app.get("/t1") @limiter.limit("5/minute") async def t1(request: Request): return PlainTextResponse("test") client = TestClient(app) for i in range(0, 10): response = client.get("/t1") assert response.status_code == 200 if i < 5 else 429 assert ( response.headers.get("X-RateLimit-Limit") is not None if i < 5 else True ) assert response.headers.get("Retry-After") is not None if i < 5 else True def test_single_decorator_not_response(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr) @app.get("/t1") @limiter.limit("5/minute") async def t1(request: Request, response: Response): return {"key": "value"} client = TestClient(app) for i in range(0, 10): response = client.get("/t1") assert response.status_code == 200 if i < 5 else 429 def test_single_decorator_not_response_with_headers(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr, headers_enabled=True) @app.get("/t1") @limiter.limit("5/minute") async def t1(request: Request, response: Response): return {"key": "value"} client = TestClient(app) for i in range(0, 10): response = client.get("/t1") assert response.status_code == 200 if i < 5 else 429 assert ( response.headers.get("X-RateLimit-Limit") is not None if i < 5 else True ) assert response.headers.get("Retry-After") is not None if i < 5 else True def test_multiple_decorators(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr) @app.get("/t1") @limiter.limit( "100 per minute", lambda: "test" ) # effectively becomes a limit for all users @limiter.limit("50/minute") # per ip as per default key_func async def t1(request: Request): return PlainTextResponse("test") with hiro.Timeline().freeze() as timeline: cli = TestClient(app) for i in range(0, 100): response = cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.2"}) assert response.status_code == 200 if i < 50 else 429 for i in range(50): assert cli.get("/t1").status_code == 200 assert cli.get("/t1").status_code == 429 assert ( cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.3"}).status_code == 429 ) def test_multiple_decorators_not_response(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr) @app.get("/t1") @limiter.limit( "100 per minute", lambda: "test" ) # effectively becomes a limit for all users @limiter.limit("50/minute") # per ip as per default key_func async def t1(request: Request, response: Response): return {"key": "value"} with hiro.Timeline().freeze() as timeline: cli = TestClient(app) for i in range(0, 100): response = cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.2"}) assert response.status_code == 200 if i < 50 else 429 for i in range(50): assert cli.get("/t1").status_code == 200 assert cli.get("/t1").status_code == 429 assert ( cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.3"}).status_code == 429 ) def test_multiple_decorators_not_response_with_headers(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr, headers_enabled=True) @app.get("/t1") @limiter.limit( "100 per minute", lambda: "test" ) # effectively becomes a limit for all users @limiter.limit("50/minute") # per ip as per default key_func async def t1(request: Request, response: Response): return {"key": "value"} with hiro.Timeline().freeze() as timeline: cli = TestClient(app) for i in range(0, 100): response = cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.2"}) assert response.status_code == 200 if i < 50 else 429 for i in range(50): assert cli.get("/t1").status_code == 200 assert cli.get("/t1").status_code == 429 assert ( cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.3"}).status_code == 429 ) def test_endpoint_missing_request_param(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr) with pytest.raises(Exception) as exc_info: @app.get("/t3") @limiter.limit("5/minute") async def t3(): return PlainTextResponse("test") assert exc_info.match( r"""^No "request" or "websocket" argument on function .*""" ) def test_endpoint_missing_request_param_sync(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr) with pytest.raises(Exception) as exc_info: @app.get("/t3_sync") @limiter.limit("5/minute") def t3(): return PlainTextResponse("test") assert exc_info.match( r"""^No "request" or "websocket" argument on function .*""" ) def test_endpoint_request_param_invalid(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr) @app.get("/t4") @limiter.limit("5/minute") async def t4(request: str = None): return PlainTextResponse("test") with pytest.raises(Exception) as exc_info: client = TestClient(app) client.get("/t4") assert exc_info.match( r"""parameter `request` must be an instance of starlette.requests.Request""" ) def test_endpoint_response_param_invalid(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr, headers_enabled=True) @app.get("/t4") @limiter.limit("5/minute") async def t4(request: Request, response: str = None): return {"key": "value"} with pytest.raises(Exception) as exc_info: client = TestClient(app) client.get("/t4") assert exc_info.match( r"""parameter `response` must be an instance of starlette.responses.Response""" ) def test_endpoint_request_param_invalid_sync(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr) @app.get("/t5") @limiter.limit("5/minute") def t5(request: str = None): return PlainTextResponse("test") with pytest.raises(Exception) as exc_info: client = TestClient(app) client.get("/t5") assert exc_info.match( r"""parameter `request` must be an instance of starlette.requests.Request""" ) def test_endpoint_response_param_invalid_sync(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr, headers_enabled=True) @app.get("/t5") @limiter.limit("5/minute") def t5(request: Request, response: str = None): return {"key": "value"} with pytest.raises(Exception) as exc_info: client = TestClient(app) client.get("/t5") assert exc_info.match( r"""parameter `response` must be an instance of starlette.responses.Response""" ) def test_dynamic_limit_provider_depending_on_key(self, build_fastapi_app): def custom_key_func(request: Request): if request.headers.get("TOKEN") == "secret": return "admin" return "user" def dynamic_limit_provider(key: str): if key == "admin": return "10/minute" return "5/minute" app, limiter = build_fastapi_app(key_func=custom_key_func) @app.get("/t1") @limiter.limit(dynamic_limit_provider) async def t1(request: Request, response: Response): return {"key": "value"} client = TestClient(app) for i in range(0, 10): response = client.get("/t1") assert response.status_code == 200 if i < 5 else 429 for i in range(0, 20): response = client.get("/t1", headers={"TOKEN": "secret"}) assert response.status_code == 200 if i < 10 else 429 def test_disabled_limiter(self, build_fastapi_app): """ Check that the limiter does nothing if disabled (both sync and async) """ app, limiter = build_fastapi_app(key_func=get_ipaddr, enabled=False) @app.get("/t1") @limiter.limit("5/minute") async def t1(request: Request): return PlainTextResponse("test") @app.get("/t2") @limiter.limit("5/minute") def t2(request: Request): return PlainTextResponse("test") @app.get("/t3") def t3(request: Request): return PlainTextResponse("also a test") client = TestClient(app) for i in range(0, 10): response = client.get("/t1") assert response.status_code == 200 for i in range(0, 10): response = client.get("/t2") assert response.status_code == 200 for i in range(0, 10): response = client.get("/t3") assert response.status_code == 200 def test_cost(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr) @app.get("/t1") @limiter.limit("50/minute", cost=10) async def t1(request: Request): return PlainTextResponse("test") @app.get("/t2") @limiter.limit("50/minute", cost=15) async def t2(request: Request): return PlainTextResponse("test") client = TestClient(app) for i in range(0, 10): response = client.get("/t1") assert response.status_code == 200 if i < 5 else 429 response = client.get("/t2") assert response.status_code == 200 if i < 3 else 429 def test_callable_cost(self, build_fastapi_app): app, limiter = build_fastapi_app(key_func=get_ipaddr) @app.get("/t1") @limiter.limit("50/minute", cost=lambda request: int(request.headers["foo"])) async def t1(request: Request): return PlainTextResponse("test") @app.get("/t2") @limiter.limit( "50/minute", cost=lambda request: int(request.headers["foo"]) * 1.5 ) async def t2(request: Request): return PlainTextResponse("test") client = TestClient(app) for i in range(0, 10): response = client.get("/t1", headers={"foo": "10"}) assert response.status_code == 200 if i < 5 else 429 response = client.get("/t2", headers={"foo": "5"}) assert response.status_code == 200 if i < 6 else 429 @pytest.mark.parametrize( "key_style", ["url", "endpoint"], ) def test_key_style(self, build_fastapi_app, key_style): app, limiter = build_fastapi_app(key_func=lambda: "mock", key_style=key_style) @app.get("/t1/{my_param}") @limiter.limit("1/minute") async def t1_func(my_param: str, request: Request): return PlainTextResponse("test") client = TestClient(app) client.get("/t1/param_one") second_call = client.get("/t1/param_two") # with the "url" key_style, since the `my_param` value changed, the storage key is different # meaning it should not raise any RateLimitExceeded error. if key_style == "url": assert second_call.status_code == 200 assert limiter._storage.get("LIMITER/mock//t1/param_one/1/1/minute") == 1 assert limiter._storage.get("LIMITER/mock//t1/param_two/1/1/minute") == 1 # However, with the `endpoint` key_style, it will use the function name (e.g: "t1_func") # meaning it will raise a RateLimitExceeded error, because no matter the parameter value # it will share the limitations. elif key_style == "endpoint": assert second_call.status_code == 429 # check that we counted 2 requests, even though we had a different value for "my_param" assert ( limiter._storage.get( "LIMITER/mock/tests.test_fastapi_extension.t1_func/1/1/minute" ) == 2 ) slowapi-0.1.9/tests/test_starlette_extension.py000066400000000000000000000320611456015001600220550ustar00rootroot00000000000000import time import hiro # type: ignore import pytest # type: ignore from starlette.requests import Request from starlette.responses import PlainTextResponse from starlette.testclient import TestClient from slowapi.util import get_ipaddr, get_remote_address from tests import TestSlowapi class TestDecorators(TestSlowapi): def test_single_decorator_async(self, build_starlette_app): app, limiter = build_starlette_app(key_func=get_ipaddr) @limiter.limit("5/minute") async def t1(request: Request): return PlainTextResponse("test") app.add_route("/t1", t1) client = TestClient(app) for i in range(0, 10): response = client.get("/t1") assert response.status_code == 200 if i < 5 else 429 if i < 5: assert response.text == "test" def test_single_decorator_sync(self, build_starlette_app): app, limiter = build_starlette_app(key_func=get_ipaddr) @limiter.limit("5/minute") def t1(request: Request): return PlainTextResponse("test") app.add_route("/t1", t1) client = TestClient(app) for i in range(0, 10): response = client.get("/t1") assert response.status_code == 200 if i < 5 else 429 if i < 5: assert response.text == "test" def test_shared_decorator(self, build_starlette_app): app, limiter = build_starlette_app(key_func=get_ipaddr) shared_lim = limiter.shared_limit("5/minute", "somescope") @shared_lim def t1(request: Request): return PlainTextResponse("test") @shared_lim def t2(request: Request): return PlainTextResponse("test") app.add_route("/t1", t1) app.add_route("/t2", t2) client = TestClient(app) for i in range(0, 10): response = client.get("/t1") assert response.status_code == 200 if i < 5 else 429 # the shared limit has already been hit via t1 assert client.get("/t2").status_code == 429 def test_multiple_decorators(self, build_starlette_app): app, limiter = build_starlette_app(key_func=get_ipaddr) @limiter.limit("10 per minute", lambda: "test") @limiter.limit("5/minute") # per ip as per default key_func async def t1(request: Request): return PlainTextResponse("test") app.add_route("/t1", t1) with hiro.Timeline().freeze() as timeline: cli = TestClient(app) for i in range(0, 10): response = cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.2"}) assert response.status_code == 200 if i < 5 else 429 for i in range(5): assert cli.get("/t1").status_code == 200 assert cli.get("/t1").status_code == 429 assert ( cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.3"}).status_code == 429 ) def test_multiple_decorators_with_headers(self, build_starlette_app): app, limiter = build_starlette_app(key_func=get_ipaddr, headers_enabled=True) @limiter.limit("10 per minute", lambda: "test") @limiter.limit("5/minute") # per ip as per default key_func async def t1(request: Request): return PlainTextResponse("test") app.add_route("/t1", t1) with hiro.Timeline().freeze() as timeline: cli = TestClient(app) for i in range(0, 10): response = cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.2"}) assert response.status_code == 200 if i < 5 else 429 assert response.headers.get("Retry-After") if i < 5 else True for i in range(5): assert cli.get("/t1").status_code == 200 assert cli.get("/t1").status_code == 429 assert ( cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.3"}).status_code == 429 ) def test_headers_no_breach(self, build_starlette_app): app, limiter = build_starlette_app( headers_enabled=True, key_func=get_remote_address ) @app.route("/t1") @limiter.limit("10/minute") def t1(request: Request): return PlainTextResponse("test") @app.route("/t2") @limiter.limit("2/second; 5 per minute; 10/hour") def t2(request: Request): return PlainTextResponse("test") with hiro.Timeline().freeze(): with TestClient(app) as cli: resp = cli.get("/t1") assert resp.headers.get("X-RateLimit-Limit") == "10" assert resp.headers.get("X-RateLimit-Remaining") == "9" assert resp.headers.get("X-RateLimit-Reset") == str( int(time.time() + 61) ) assert resp.headers.get("Retry-After") == str(60) resp = cli.get("/t2") assert resp.headers.get("X-RateLimit-Limit") == "2" assert resp.headers.get("X-RateLimit-Remaining") == "1" assert resp.headers.get("X-RateLimit-Reset") == str( int(time.time() + 2) ) assert resp.headers.get("Retry-After") == str(1) def test_headers_breach(self, build_starlette_app): app, limiter = build_starlette_app( headers_enabled=True, key_func=get_remote_address ) @app.route("/t1") @limiter.limit("2/second; 10 per minute; 20/hour") def t(request: Request): return PlainTextResponse("test") with hiro.Timeline().freeze() as timeline: with TestClient(app) as cli: for i in range(11): resp = cli.get("/t1") timeline.forward(1) assert resp.headers.get("X-RateLimit-Limit") == "10" assert resp.headers.get("X-RateLimit-Remaining") == "0" assert resp.headers.get("X-RateLimit-Reset") == str( int(time.time() + 50) ) assert resp.headers.get("Retry-After") == str(int(50)) def test_retry_after(self, build_starlette_app): # FIXME: this test is not actually running! app, limiter = build_starlette_app( headers_enabled=True, key_func=get_remote_address ) @app.route("/t1") @limiter.limit("1/minute") def t(request: Request): return PlainTextResponse("test") with hiro.Timeline().freeze() as timeline: with TestClient(app) as cli: resp = cli.get("/t1") retry_after = int(resp.headers.get("Retry-After")) assert retry_after > 0 timeline.forward(retry_after) resp = cli.get("/t1") assert resp.status_code == 200 def test_exempt_decorator(self, build_starlette_app): app, limiter = build_starlette_app( headers_enabled=True, key_func=get_remote_address, default_limits=["1/minute"], ) @app.route("/t1") def t1(request: Request): return PlainTextResponse("test") with TestClient(app) as cli: resp = cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.10"}) assert resp.status_code == 200 resp2 = cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.10"}) assert resp2.status_code == 429 @app.route("/t2") @limiter.exempt def t2(request: Request): """Exempt a sync route""" return PlainTextResponse("test") with TestClient(app) as cli: resp = cli.get("/t2", headers={"X_FORWARDED_FOR": "127.0.0.10"}) assert resp.status_code == 200 resp2 = cli.get("/t2", headers={"X_FORWARDED_FOR": "127.0.0.10"}) assert resp2.status_code == 200 @app.route("/t3") @limiter.exempt async def t3(request: Request): """Exempt an async route""" return PlainTextResponse("test") with TestClient(app) as cli: resp = cli.get("/t3", headers={"X_FORWARDED_FOR": "127.0.0.10"}) assert resp.status_code == 200 resp2 = cli.get("/t3", headers={"X_FORWARDED_FOR": "127.0.0.10"}) assert resp2.status_code == 200 # todo: more tests - see https://github.com/alisaifee/flask-limiter/blob/55df08f14143a7e918fc033067a494248ab6b0c5/tests/test_decorators.py#L187 def test_default_and_decorator_limit_merging(self, build_starlette_app): app, limiter = build_starlette_app( key_func=lambda: "test", default_limits=["10/minute"] ) @limiter.limit("5 per minute", key_func=get_ipaddr, override_defaults=False) async def t1(request: Request): return PlainTextResponse("test") app.add_route("/t1", t1) with hiro.Timeline().freeze() as timeline: cli = TestClient(app) for i in range(0, 10): response = cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.2"}) assert response.status_code == 200 if i < 5 else 429 for i in range(5): assert cli.get("/t1").status_code == 200 assert cli.get("/t1").status_code == 429 assert ( cli.get("/t1", headers={"X_FORWARDED_FOR": "127.0.0.3"}).status_code == 429 ) def test_cost(self, build_starlette_app): app, limiter = build_starlette_app(key_func=get_ipaddr) @limiter.limit("50/minute", cost=10) async def t1(request: Request): return PlainTextResponse("test") app.add_route("/t1", t1) @limiter.limit("50/minute", cost=15) async def t2(request: Request): return PlainTextResponse("test") app.add_route("/t2", t2) client = TestClient(app) for i in range(0, 10): response = client.get("/t1") assert response.status_code == 200 if i < 5 else 429 if i < 5: assert response.text == "test" else: assert "error" in response.json() response = client.get("/t2") assert response.status_code == 200 if i < 3 else 429 if i < 3: assert response.text == "test" else: assert "error" in response.json() def test_callable_cost(self, build_starlette_app): app, limiter = build_starlette_app(key_func=get_ipaddr) @limiter.limit("50/minute", cost=lambda request: int(request.headers["foo"])) async def t1(request: Request): return PlainTextResponse("test") app.add_route("/t1", t1) @limiter.limit( "50/minute", cost=lambda request: int(request.headers["foo"]) * 1.5 ) async def t2(request: Request): return PlainTextResponse("test") app.add_route("/t2", t2) client = TestClient(app) for i in range(0, 10): response = client.get("/t1", headers={"foo": "10"}) assert response.status_code == 200 if i < 5 else 429 if i < 5: assert response.text == "test" else: assert "error" in response.json() response = client.get("/t2", headers={"foo": "5"}) assert response.status_code == 200 if i < 6 else 429 if i < 6: assert response.text == "test" else: assert "error" in response.json() @pytest.mark.parametrize( "key_style", ["url", "endpoint"], ) def test_key_style(self, build_starlette_app, key_style): app, limiter = build_starlette_app(key_func=lambda: "mock", key_style=key_style) @limiter.limit("1/minute") async def t1_func(request: Request): return PlainTextResponse("test") app.add_route("/t1/{my_param}", t1_func) client = TestClient(app) client.get("/t1/param_one") second_call = client.get("/t1/param_two") # with the "url" key_style, since the `my_param` value changed, the storage key is different # meaning it should not raise any RateLimitExceeded error. if key_style == "url": assert second_call.status_code == 200 assert limiter._storage.get("LIMITER/mock//t1/param_one/1/1/minute") == 1 assert limiter._storage.get("LIMITER/mock//t1/param_two/1/1/minute") == 1 # However, with the `endpoint` key_style, it will use the function name (e.g: "t1_func") # meaning it will raise a RateLimitExceeded error, because no matter the parameter value # it will share the limitations. elif key_style == "endpoint": assert second_call.status_code == 429 # check that we counted 2 requests, even though we had a different value for "my_param" assert ( limiter._storage.get( "LIMITER/mock/tests.test_starlette_extension.t1_func/1/1/minute" ) == 2 )