├── .github └── workflows │ ├── pr_python_tests.yml │ ├── push_linting.yml │ └── release-please.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .rtx.toml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── app └── main.py ├── docker-compose.yml ├── fastapi_simple_security ├── __init__.py ├── _security_secret.py ├── _sqlite_access.py ├── endpoints.py └── security_api_key.py ├── images ├── auth_endpoints.png ├── new_api_key.png ├── secret.png ├── secret_header.png └── secure_endpoint.png ├── poetry.lock ├── pylintrc ├── pyproject.toml ├── renovate.json └── tests ├── conftest.py ├── test_functional.py └── test_security.py /.github/workflows/pr_python_tests.yml: -------------------------------------------------------------------------------- 1 | name: Python Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [main, master] 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | 11 | environment: 12 | name: testing 13 | 14 | steps: 15 | # Get the files 16 | - uses: actions/checkout@v3 17 | 18 | # Python setup 19 | - name: Set up Python 3.11 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: "3.11" 23 | 24 | - name: Install poetry 25 | uses: abatilo/actions-poetry@v2.2.0 26 | with: 27 | poetry-version: 1.4.2 28 | 29 | - name: Install python dependencies 30 | run: | 31 | poetry install 32 | 33 | - name: Run Python tests 34 | run: | 35 | poetry run coverage run -m pytest -s tests 36 | 37 | - name: Generate and upload coverage reports to Codecov 38 | run: | 39 | poetry run coverage xml 40 | curl -Os https://uploader.codecov.io/latest/linux/codecov 41 | chmod +x codecov 42 | ./codecov -t ${{ secrets.CODECOV_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/push_linting.yml: -------------------------------------------------------------------------------- 1 | name: Python linting 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Install Python 11 | uses: actions/setup-python@v4 12 | with: 13 | python-version: "3.11" 14 | - name: Install dependencies 15 | run: | 16 | python -m pip install --upgrade pip 17 | pip install ruff 18 | - name: Run Ruff 19 | run: ruff --format=github . 20 | 21 | format: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Run Black 26 | uses: psf/black@stable 27 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main, master] 4 | 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | 9 | name: release-please 10 | 11 | jobs: 12 | release-please: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: google-github-actions/release-please-action@v3 16 | with: 17 | release-type: python 18 | package-name: release-please-action 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # IDE files 132 | .idea/ 133 | 134 | # DB files 135 | *.db 136 | 137 | .vscode 138 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | # https://github.com/pre-commit/pre-commit-hooks#pre-commit-hooks 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.3.0 5 | hooks: 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: check-added-large-files 10 | 11 | # https://pre-commit.com/#2-add-a-pre-commit-configuration 12 | - repo: https://github.com/psf/black 13 | rev: 23.1.0 14 | hooks: 15 | - id: black 16 | language_version: python3.11 17 | 18 | # Linting with Ruff 19 | - repo: https://github.com/charliermarsh/ruff-pre-commit 20 | rev: "v0.0.221" 21 | hooks: 22 | - id: ruff 23 | 24 | # Validating conventional pre-commit messages 25 | - repo: https://github.com/compilerla/conventional-pre-commit 26 | rev: "v2.1.1" 27 | hooks: 28 | - id: conventional-pre-commit 29 | stages: [commit-msg] 30 | args: [feat, fix, doc] 31 | -------------------------------------------------------------------------------- /.rtx.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | python = { version = '3.11', virtualenv = '.venv' } 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.3.0](https://github.com/mrtolkien/fastapi_simple_security/compare/1.2.0...v1.3.0) (2023-07-18) 4 | 5 | 6 | ### Features 7 | 8 | * using secrets.compare_digest and making control flow clearer ([0a48b8b](https://github.com/mrtolkien/fastapi_simple_security/commit/0a48b8bdbe8daf2a031b3bc9b2016201db05b50c)) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * add Google Release Please action ([02a296e](https://github.com/mrtolkien/fastapi_simple_security/commit/02a296e73d07d4f13211ebc7dd7f8c955377fe48)) 14 | * add release-please on master ([935b9a0](https://github.com/mrtolkien/fastapi_simple_security/commit/935b9a06dd3a048ea33de4862f2cc2bea5b21036)) 15 | * add rtx dependency + update poetry lock ([cd27a7a](https://github.com/mrtolkien/fastapi_simple_security/commit/cd27a7a18a759359b20d3ddf3c6eac45b2f982ed)) 16 | * better imports ([361c1cc](https://github.com/mrtolkien/fastapi_simple_security/commit/361c1cce2fa49fd0436fbff2ad87c8e4469f4e2c)) 17 | * dev dependencies cleanup ([d8ae38e](https://github.com/mrtolkien/fastapi_simple_security/commit/d8ae38e95eb1cdb60566adb71915b0231c8fb416)) 18 | * formatting fixes + adding httpx as dev depedency ([c4b4005](https://github.com/mrtolkien/fastapi_simple_security/commit/c4b40051cc942100623e1be683eb7066fe0bcedd)) 19 | * ignore vscode conf ([5795d2f](https://github.com/mrtolkien/fastapi_simple_security/commit/5795d2f7ffc363043f7d81b6de36ed22e4465a69)) 20 | * move CI check to Ruff ([685e5ed](https://github.com/mrtolkien/fastapi_simple_security/commit/685e5ed22d5e81ce0a45c7bbf90e795304e1cff3)) 21 | * remove VScode-specific files ([56aed6b](https://github.com/mrtolkien/fastapi_simple_security/commit/56aed6baad8d1d1bf53097c538025db806ae5788)) 22 | * update pre-commit with Ruff ([6e18c74](https://github.com/mrtolkien/fastapi_simple_security/commit/6e18c74aa96259d59b60e93177a06145319e835d)) 23 | * update python and poetry versions in CI tests ([539fe7e](https://github.com/mrtolkien/fastapi_simple_security/commit/539fe7e7f590c6be000a25e96072d884c9a8a132)) 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:latest 2 | 3 | # Installing dev depedencies 4 | RUN pip install pytest python-dotenv 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tolki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI simple security 2 | 3 | [![codecov](https://codecov.io/github/mrtolkien/fastapi_simple_security/branch/master/graph/badge.svg?token=8VIKJ9J3XF)](https://codecov.io/github/mrtolkien/fastapi_simple_security) 4 | [![Python Tests](https://github.com/mrtolkien/fastapi_simple_security/actions/workflows/pr_python_tests.yml/badge.svg)](https://github.com/mrtolkien/fastapi_simple_security/actions/workflows/pr_python_tests.yml) 5 | [![Linting](https://github.com/mrtolkien/fastapi_simple_security/actions/workflows/push_linting.yml/badge.svg)](https://github.com/mrtolkien/fastapi_simple_security/actions/workflows/push_linting.yml) 6 | 7 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 8 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 9 | [![pre-commit enabled][pre-commit badge]][pre-commit project] 10 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) 11 | 12 | [pre-commit badge]: 13 | [pre-commit project]: 14 | 15 | API key based security package for FastAPI, focused on simplicity of use: 16 | 17 | - Full functionality out of the box, no configuration required 18 | - API key security with local `sqlite` backend, working with both header and query parameters 19 | - Default 15 days deprecation for generated API keys 20 | - Key creation, revocation, renewing, and usage logs handled through administrator endpoints 21 | - No dependencies, only requiring `FastAPI` and the python standard library 22 | 23 | This module cannot be used for any kind of distributed deployment. It's goal is to help have some basic security features 24 | for simple one-server API deployments, mostly during development. 25 | 26 | ## Installation 27 | 28 | `pip install fastapi_simple_security` 29 | 30 | ### Usage 31 | 32 | ### Creating an application 33 | 34 | ```python 35 | from fastapi_simple_security import api_key_router, api_key_security 36 | from fastapi import Depends, FastAPI 37 | 38 | app = FastAPI() 39 | 40 | app.include_router(api_key_router, prefix="/auth", tags=["_auth"]) 41 | 42 | @app.get("/secure", dependencies=[Depends(api_key_security)]) 43 | async def secure_endpoint(): 44 | return {"message": "This is a secure endpoint"} 45 | ``` 46 | 47 | Resulting app is: 48 | 49 | ![app](images/auth_endpoints.png) 50 | 51 | ### API key creation through docs 52 | 53 | Start your API and check the logs for the automatically generated secret key if you did not provide one through 54 | environment variables. 55 | 56 | ![secret](images/secret.png) 57 | 58 | Go to `/docs` on your API and inform this secret key in the `Authorize/Secret header` box. 59 | All the administrator endpoints only support header security to make sure the secret key is not inadvertently 60 | shared when sharing an URL. 61 | 62 | ![secret_header](images/secret_header.png) 63 | 64 | Then, you can use `/auth/new` to generate a new API key. 65 | 66 | ![api key](images/new_api_key.png) 67 | 68 | And finally, you can use this API key to access the secure endpoint. 69 | 70 | ![secure endpoint](images/secure_endpoint.png) 71 | 72 | ### API key creation in python 73 | 74 | You can of course automate API key acquisition through python with `requests` and directly querying the endpoints. 75 | 76 | If you do so, you can hide the endpoints from your API documentation with the environment variable 77 | `FASTAPI_SIMPLE_SECURITY_HIDE_DOCS`. 78 | 79 | ## Configuration 80 | 81 | Environment variables: 82 | 83 | - `FASTAPI_SIMPLE_SECURITY_SECRET`: Secret administrator key 84 | 85 | - Generated automatically on server startup if not provided 86 | - Allows generation of new API keys, revoking of existing ones, and API key usage view 87 | - It being compromised compromises the security of the API 88 | 89 | - `FASTAPI_SIMPLE_SECURITY_HIDE_DOCS`: Whether or not to hide the API key related endpoints from the documentation 90 | - `FASTAPI_SIMPLE_SECURITY_DB_LOCATION`: Location of the local sqlite database file 91 | - `sqlite.db` in the running directory by default 92 | - When running the app inside Docker, use a bind mount for persistence 93 | - `FAST_API_SIMPLE_SECURITY_AUTOMATIC_EXPIRATION`: Duration, in days, until an API key is deemed expired 94 | - 15 days by default 95 | 96 | ## Contributing 97 | 98 | ### Setting up python environment 99 | 100 | ```shell script 101 | poetry install 102 | poetry shell 103 | ``` 104 | 105 | ### Setting up pre-commit hooks 106 | 107 | ```shell script 108 | pre-commit install 109 | ``` 110 | 111 | ### Running tests 112 | 113 | ```shell script 114 | pytest 115 | ``` 116 | 117 | ### Running the dev environment 118 | 119 | The attached docker image runs a test app on `localhost:8080` with secret key `TEST_SECRET`. Run it with: 120 | 121 | ```shell script 122 | docker-compose build && docker-compose up 123 | ``` 124 | 125 | ## Needed contributions 126 | 127 | - More options with sensible defaults 128 | - Logging per API key? 129 | - More back-end options for API key storage? 130 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | """Sample app client used for testing. 2 | """ 3 | from fastapi import Depends, FastAPI 4 | 5 | import fastapi_simple_security 6 | 7 | app = FastAPI() 8 | 9 | 10 | @app.get("/unsecured") 11 | async def unsecured_endpoint(): 12 | return {"message": "This is an unsecured endpoint"} 13 | 14 | 15 | @app.get("/secure", dependencies=[Depends(fastapi_simple_security.api_key_security)]) 16 | async def secure_endpoint(): 17 | return {"message": "This is a secure endpoint"} 18 | 19 | 20 | app.include_router( 21 | fastapi_simple_security.api_key_router, prefix="/auth", tags=["_auth"] 22 | ) 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | api: 5 | image: fastapi_simple_security 6 | container_name: fastapi_simple_security_dev 7 | restart: always 8 | build: 9 | context: . 10 | dockerfile: Dockerfile 11 | environment: 12 | - FASTAPI_SIMPLE_SECURITY_SECRET=TEST_SECRET 13 | ports: 14 | - target: 80 15 | published: 8080 16 | volumes: 17 | - type: bind 18 | source: ./app 19 | target: /app 20 | - type: bind 21 | source: ./fastapi_simple_security 22 | target: /app/fastapi_simple_security 23 | command: /start-reload.sh 24 | -------------------------------------------------------------------------------- /fastapi_simple_security/__init__.py: -------------------------------------------------------------------------------- 1 | """Simple FastAPI security with a local SQLite database. 2 | """ 3 | from fastapi_simple_security.endpoints import api_key_router 4 | from fastapi_simple_security.security_api_key import api_key_security 5 | 6 | __all__ = ["api_key_router", "api_key_security"] 7 | -------------------------------------------------------------------------------- /fastapi_simple_security/_security_secret.py: -------------------------------------------------------------------------------- 1 | """Secret dependency. 2 | """ 3 | import os 4 | import secrets 5 | import uuid 6 | import warnings 7 | from typing import Optional 8 | 9 | from fastapi import Security 10 | from fastapi.security import APIKeyHeader 11 | from starlette.exceptions import HTTPException 12 | from starlette.status import HTTP_403_FORBIDDEN 13 | 14 | 15 | class GhostLoadedSecret: 16 | """Ghost-loaded secret handler""" 17 | 18 | def __init__(self) -> None: 19 | self._secret = None 20 | 21 | @property 22 | def value(self): 23 | if self._secret: 24 | return self._secret 25 | 26 | else: 27 | self._secret = self.get_secret_value() 28 | return self.value 29 | 30 | def get_secret_value(self): 31 | try: 32 | secret_value = os.environ["FASTAPI_SIMPLE_SECURITY_SECRET"] 33 | 34 | except KeyError: 35 | secret_value = str(uuid.uuid4()) 36 | 37 | warnings.warn( 38 | f"ENVIRONMENT VARIABLE 'FASTAPI_SIMPLE_SECURITY_SECRET' NOT FOUND\n" 39 | f"\tGenerated a single-use secret key for this session:\n" 40 | f"\t{secret_value=}" 41 | ) 42 | 43 | return secret_value 44 | 45 | 46 | secret = GhostLoadedSecret() 47 | 48 | SECRET_KEY_NAME = "secret-key" 49 | 50 | secret_header = APIKeyHeader( 51 | name=SECRET_KEY_NAME, scheme_name="Secret header", auto_error=False 52 | ) 53 | 54 | 55 | async def secret_based_security(header_param: Optional[str] = Security(secret_header)): 56 | """ 57 | Args: 58 | header_param: parsed header field secret_header 59 | 60 | Returns: 61 | True if the authentication was successful 62 | 63 | Raises: 64 | HTTPException if the authentication failed 65 | """ 66 | 67 | if not header_param: 68 | raise HTTPException( 69 | status_code=HTTP_403_FORBIDDEN, 70 | detail="secret_key must be passed as a header field", 71 | ) 72 | 73 | # We simply return True if the given secret-key has the right value 74 | if not secrets.compare_digest(header_param, secret.value): 75 | raise HTTPException( 76 | status_code=HTTP_403_FORBIDDEN, 77 | detail="Wrong secret key. If not set through environment variable \ 78 | 'FASTAPI_SIMPLE_SECURITY_SECRET', it was " 79 | "generated automatically at startup and appears in the server logs.", 80 | ) 81 | 82 | else: 83 | return True 84 | -------------------------------------------------------------------------------- /fastapi_simple_security/_sqlite_access.py: -------------------------------------------------------------------------------- 1 | """Interaction with SQLite database. 2 | """ 3 | import os 4 | import sqlite3 5 | import threading 6 | import uuid 7 | from datetime import datetime, timedelta 8 | from typing import List, Optional, Tuple 9 | 10 | from fastapi import HTTPException 11 | from starlette.status import HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY 12 | 13 | 14 | class SQLiteAccess: 15 | """Class handling SQLite connection and writes""" 16 | 17 | # TODO This should not be a class, a fully functional approach is better 18 | 19 | def __init__(self): 20 | try: 21 | self.db_location = os.environ["FASTAPI_SIMPLE_SECURITY_DB_LOCATION"] 22 | except KeyError: 23 | self.db_location = "sqlite.db" 24 | 25 | try: 26 | self.expiration_limit = int( 27 | os.environ["FAST_API_SIMPLE_SECURITY_AUTOMATIC_EXPIRATION"] 28 | ) 29 | except KeyError: 30 | self.expiration_limit = 15 31 | 32 | self.init_db() 33 | 34 | def init_db(self): 35 | with sqlite3.connect(self.db_location) as connection: 36 | c = connection.cursor() 37 | # Create database 38 | c.execute( 39 | """ 40 | CREATE TABLE IF NOT EXISTS fastapi_simple_security ( 41 | api_key TEXT PRIMARY KEY, 42 | is_active INTEGER, 43 | never_expire INTEGER, 44 | expiration_date TEXT, 45 | latest_query_date TEXT, 46 | total_queries INTEGER) 47 | """ 48 | ) 49 | connection.commit() 50 | # Migration: Add api key name 51 | try: 52 | c.execute("ALTER TABLE fastapi_simple_security ADD COLUMN name TEXT") 53 | connection.commit() 54 | except sqlite3.OperationalError: 55 | pass # Column already exist 56 | 57 | def create_key(self, name, never_expire) -> str: 58 | api_key = str(uuid.uuid4()) 59 | 60 | with sqlite3.connect(self.db_location) as connection: 61 | c = connection.cursor() 62 | c.execute( 63 | """ 64 | INSERT INTO fastapi_simple_security 65 | (api_key, is_active, never_expire, expiration_date, \ 66 | latest_query_date, total_queries, name) 67 | VALUES(?, ?, ?, ?, ?, ?, ?) 68 | """, 69 | ( 70 | api_key, 71 | 1, 72 | 1 if never_expire else 0, 73 | ( 74 | datetime.utcnow() + timedelta(days=self.expiration_limit) 75 | ).isoformat(timespec="seconds"), 76 | None, 77 | 0, 78 | name, 79 | ), 80 | ) 81 | connection.commit() 82 | 83 | return api_key 84 | 85 | def renew_key(self, api_key: str, new_expiration_date: str) -> Optional[str]: 86 | with sqlite3.connect(self.db_location) as connection: 87 | c = connection.cursor() 88 | 89 | # We run the query like check_key but will use the response differently 90 | c.execute( 91 | """ 92 | SELECT is_active, total_queries, expiration_date, never_expire 93 | FROM fastapi_simple_security 94 | WHERE api_key = ?""", 95 | (api_key,), 96 | ) 97 | 98 | response = c.fetchone() 99 | 100 | # API key not found 101 | if not response: 102 | raise HTTPException( 103 | status_code=HTTP_404_NOT_FOUND, detail="API key not found" 104 | ) 105 | 106 | response_lines = [] 107 | 108 | # Previously revoked key. Issue a text warning and reactivate it. 109 | if response[0] == 0: 110 | response_lines.append( 111 | "This API key was revoked and has been reactivated." 112 | ) 113 | 114 | # Without an expiration date, we set it here 115 | if not new_expiration_date: 116 | parsed_expiration_date = ( 117 | datetime.utcnow() + timedelta(days=self.expiration_limit) 118 | ).isoformat(timespec="seconds") 119 | 120 | else: 121 | try: 122 | # We parse and re-write to the right timespec 123 | parsed_expiration_date = datetime.fromisoformat( 124 | new_expiration_date 125 | ).isoformat(timespec="seconds") 126 | except ValueError as exc: 127 | raise HTTPException( 128 | status_code=HTTP_422_UNPROCESSABLE_ENTITY, 129 | detail="The expiration date could not be parsed. \ 130 | Please use ISO 8601.", 131 | ) from exc 132 | 133 | c.execute( 134 | """ 135 | UPDATE fastapi_simple_security 136 | SET expiration_date = ?, is_active = 1 137 | WHERE api_key = ? 138 | """, 139 | ( 140 | parsed_expiration_date, 141 | api_key, 142 | ), 143 | ) 144 | 145 | connection.commit() 146 | 147 | response_lines.append( 148 | f"The new expiration date for the API key is {parsed_expiration_date}" 149 | ) 150 | 151 | return " ".join(response_lines) 152 | 153 | def revoke_key(self, api_key: str): 154 | """ 155 | Revokes an API key 156 | 157 | Args: 158 | api_key: the API key to revoke 159 | """ 160 | with sqlite3.connect(self.db_location) as connection: 161 | c = connection.cursor() 162 | 163 | c.execute( 164 | """ 165 | UPDATE fastapi_simple_security 166 | SET is_active = 0 167 | WHERE api_key = ? 168 | """, 169 | (api_key,), 170 | ) 171 | 172 | connection.commit() 173 | 174 | def check_key(self, api_key: str) -> bool: 175 | """ 176 | Checks if an API key is valid 177 | 178 | Args: 179 | api_key: the API key to validate 180 | """ 181 | 182 | with sqlite3.connect(self.db_location) as connection: 183 | c = connection.cursor() 184 | 185 | c.execute( 186 | """ 187 | SELECT is_active, total_queries, expiration_date, never_expire 188 | FROM fastapi_simple_security 189 | WHERE api_key = ?""", 190 | (api_key,), 191 | ) 192 | 193 | response = c.fetchone() 194 | 195 | if ( 196 | # Cannot fetch a row 197 | not response 198 | # Inactive 199 | or response[0] != 1 200 | # Expired key 201 | or ( 202 | (not response[3]) 203 | and (datetime.fromisoformat(response[2]) < datetime.utcnow()) 204 | ) 205 | ): 206 | # The key is not valid 207 | return False 208 | else: 209 | # The key is valid 210 | 211 | # We run the logging in a separate thread as writing takes some time 212 | threading.Thread( 213 | target=self._update_usage, 214 | args=( 215 | api_key, 216 | response[1], 217 | ), 218 | ).start() 219 | 220 | # We return directly 221 | return True 222 | 223 | def _update_usage(self, api_key: str, usage_count: int): 224 | with sqlite3.connect(self.db_location) as connection: 225 | c = connection.cursor() 226 | 227 | # If we get there, this means it’s an active API key that’s in the database 228 | # We update the table 229 | c.execute( 230 | """ 231 | UPDATE fastapi_simple_security 232 | SET total_queries = ?, latest_query_date = ? 233 | WHERE api_key = ? 234 | """, 235 | ( 236 | usage_count + 1, 237 | datetime.utcnow().isoformat(timespec="seconds"), 238 | api_key, 239 | ), 240 | ) 241 | 242 | connection.commit() 243 | 244 | def get_usage_stats(self) -> List[Tuple[str, bool, bool, str, str, int]]: 245 | """ 246 | Returns usage stats for all API keys 247 | 248 | Returns: 249 | a list of tuples with values being api_key, is_active, expiration_date, \ 250 | latest_query_date, and total_queries 251 | """ 252 | with sqlite3.connect(self.db_location) as connection: 253 | c = connection.cursor() 254 | 255 | c.execute( 256 | """ 257 | SELECT api_key, is_active, never_expire, expiration_date, \ 258 | latest_query_date, total_queries, name 259 | FROM fastapi_simple_security 260 | ORDER BY latest_query_date DESC 261 | """, 262 | ) 263 | 264 | response = c.fetchall() 265 | 266 | return response 267 | 268 | 269 | sqlite_access = SQLiteAccess() 270 | -------------------------------------------------------------------------------- /fastapi_simple_security/endpoints.py: -------------------------------------------------------------------------------- 1 | """Endpoints defined by the dependency. 2 | """ 3 | import os 4 | from typing import List, Optional 5 | 6 | from fastapi import APIRouter, Depends, Query 7 | from pydantic import BaseModel 8 | 9 | from fastapi_simple_security._security_secret import secret_based_security 10 | from fastapi_simple_security._sqlite_access import sqlite_access 11 | 12 | api_key_router = APIRouter() 13 | 14 | show_endpoints = "FASTAPI_SIMPLE_SECURITY_HIDE_DOCS" not in os.environ 15 | 16 | 17 | @api_key_router.get( 18 | "/new", 19 | dependencies=[Depends(secret_based_security)], 20 | include_in_schema=show_endpoints, 21 | ) 22 | def get_new_api_key( 23 | name: str = Query( 24 | None, 25 | description="set API key name", 26 | ), 27 | never_expires: bool = Query( 28 | False, 29 | description="if set, the created API key will never be considered expired", 30 | ), 31 | ) -> str: 32 | """ 33 | Returns: 34 | api_key: a newly generated API key 35 | """ 36 | return sqlite_access.create_key(name, never_expires) 37 | 38 | 39 | @api_key_router.get( 40 | "/revoke", 41 | dependencies=[Depends(secret_based_security)], 42 | include_in_schema=show_endpoints, 43 | ) 44 | def revoke_api_key( 45 | api_key: str = Query(..., alias="api-key", description="the api_key to revoke") 46 | ): 47 | """ 48 | Revokes the usage of the given API key 49 | 50 | """ 51 | return sqlite_access.revoke_key(api_key) 52 | 53 | 54 | @api_key_router.get( 55 | "/renew", 56 | dependencies=[Depends(secret_based_security)], 57 | include_in_schema=show_endpoints, 58 | ) 59 | def renew_api_key( 60 | api_key: str = Query(..., alias="api-key", description="the API key to renew"), 61 | expiration_date: str = Query( 62 | None, 63 | alias="expiration-date", 64 | description="the new expiration date in ISO format", 65 | ), 66 | ): 67 | """ 68 | Renews the chosen API key, reactivating it if it was revoked. 69 | """ 70 | return sqlite_access.renew_key(api_key, expiration_date) 71 | 72 | 73 | class UsageLog(BaseModel): 74 | api_key: str 75 | name: Optional[str] 76 | is_active: bool 77 | never_expire: bool 78 | expiration_date: str 79 | latest_query_date: Optional[str] 80 | total_queries: int 81 | 82 | 83 | class UsageLogs(BaseModel): 84 | logs: List[UsageLog] 85 | 86 | 87 | @api_key_router.get( 88 | "/logs", 89 | dependencies=[Depends(secret_based_security)], 90 | response_model=UsageLogs, 91 | include_in_schema=show_endpoints, 92 | ) 93 | def get_api_key_usage_logs(): 94 | """ 95 | Returns usage information for all API keys 96 | """ 97 | # TODO Add some sort of filtering on older keys/unused keys? 98 | 99 | return UsageLogs( 100 | logs=[ 101 | UsageLog( 102 | api_key=row[0], 103 | is_active=row[1], 104 | never_expire=row[2], 105 | expiration_date=row[3], 106 | latest_query_date=row[4], 107 | total_queries=row[5], 108 | name=row[6], 109 | ) 110 | for row in sqlite_access.get_usage_stats() 111 | ] 112 | ) 113 | -------------------------------------------------------------------------------- /fastapi_simple_security/security_api_key.py: -------------------------------------------------------------------------------- 1 | """Main dependency for other endpoints. 2 | """ 3 | from fastapi import Security 4 | from fastapi.security import APIKeyHeader, APIKeyQuery 5 | from starlette.exceptions import HTTPException 6 | from starlette.status import HTTP_403_FORBIDDEN 7 | 8 | from fastapi_simple_security._sqlite_access import sqlite_access 9 | 10 | API_KEY_NAME = "api-key" 11 | 12 | api_key_query = APIKeyQuery( 13 | name=API_KEY_NAME, scheme_name="API key query", auto_error=False 14 | ) 15 | api_key_header = APIKeyHeader( 16 | name=API_KEY_NAME, scheme_name="API key header", auto_error=False 17 | ) 18 | 19 | 20 | async def api_key_security( 21 | query_param: str = Security(api_key_query), 22 | header_param: str = Security(api_key_header), 23 | ): 24 | if not query_param and not header_param: 25 | raise HTTPException( 26 | status_code=HTTP_403_FORBIDDEN, 27 | detail="An API key must be passed as query or header", 28 | ) 29 | 30 | elif query_param and sqlite_access.check_key(query_param): 31 | return query_param 32 | 33 | elif header_param and sqlite_access.check_key(header_param): 34 | return header_param 35 | 36 | else: 37 | raise HTTPException( 38 | status_code=HTTP_403_FORBIDDEN, detail="Wrong, revoked, or expired API key." 39 | ) 40 | -------------------------------------------------------------------------------- /images/auth_endpoints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrtolkien/fastapi_simple_security/c2db8d70795a2cac682f759296206122df64b5fb/images/auth_endpoints.png -------------------------------------------------------------------------------- /images/new_api_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrtolkien/fastapi_simple_security/c2db8d70795a2cac682f759296206122df64b5fb/images/new_api_key.png -------------------------------------------------------------------------------- /images/secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrtolkien/fastapi_simple_security/c2db8d70795a2cac682f759296206122df64b5fb/images/secret.png -------------------------------------------------------------------------------- /images/secret_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrtolkien/fastapi_simple_security/c2db8d70795a2cac682f759296206122df64b5fb/images/secret_header.png -------------------------------------------------------------------------------- /images/secure_endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrtolkien/fastapi_simple_security/c2db8d70795a2cac682f759296206122df64b5fb/images/secure_endpoint.png -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "3.7.0" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | category = "main" 8 | optional = false 9 | python-versions = ">=3.7" 10 | files = [ 11 | {file = "anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0"}, 12 | {file = "anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce"}, 13 | ] 14 | 15 | [package.dependencies] 16 | exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} 17 | idna = ">=2.8" 18 | sniffio = ">=1.1" 19 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 20 | 21 | [package.extras] 22 | doc = ["Sphinx (>=6.1.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme", "sphinxcontrib-jquery"] 23 | test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 24 | trio = ["trio (<0.22)"] 25 | 26 | [[package]] 27 | name = "astroid" 28 | version = "2.15.5" 29 | description = "An abstract syntax tree for Python with inference support." 30 | category = "dev" 31 | optional = false 32 | python-versions = ">=3.7.2" 33 | files = [ 34 | {file = "astroid-2.15.5-py3-none-any.whl", hash = "sha256:078e5212f9885fa85fbb0cf0101978a336190aadea6e13305409d099f71b2324"}, 35 | {file = "astroid-2.15.5.tar.gz", hash = "sha256:1039262575027b441137ab4a62a793a9b43defb42c32d5670f38686207cd780f"}, 36 | ] 37 | 38 | [package.dependencies] 39 | lazy-object-proxy = ">=1.4.0" 40 | typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} 41 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} 42 | wrapt = [ 43 | {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, 44 | {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, 45 | ] 46 | 47 | [[package]] 48 | name = "black" 49 | version = "22.12.0" 50 | description = "The uncompromising code formatter." 51 | category = "dev" 52 | optional = false 53 | python-versions = ">=3.7" 54 | files = [ 55 | {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, 56 | {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, 57 | {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, 58 | {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, 59 | {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, 60 | {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, 61 | {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, 62 | {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, 63 | {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, 64 | {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, 65 | {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, 66 | {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, 67 | ] 68 | 69 | [package.dependencies] 70 | click = ">=8.0.0" 71 | mypy-extensions = ">=0.4.3" 72 | pathspec = ">=0.9.0" 73 | platformdirs = ">=2" 74 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 75 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} 76 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 77 | 78 | [package.extras] 79 | colorama = ["colorama (>=0.4.3)"] 80 | d = ["aiohttp (>=3.7.4)"] 81 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 82 | uvloop = ["uvloop (>=0.15.2)"] 83 | 84 | [[package]] 85 | name = "certifi" 86 | version = "2023.5.7" 87 | description = "Python package for providing Mozilla's CA Bundle." 88 | category = "dev" 89 | optional = false 90 | python-versions = ">=3.6" 91 | files = [ 92 | {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, 93 | {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, 94 | ] 95 | 96 | [[package]] 97 | name = "cfgv" 98 | version = "3.3.1" 99 | description = "Validate configuration and produce human readable error messages." 100 | category = "dev" 101 | optional = false 102 | python-versions = ">=3.6.1" 103 | files = [ 104 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 105 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 106 | ] 107 | 108 | [[package]] 109 | name = "charset-normalizer" 110 | version = "3.1.0" 111 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 112 | category = "dev" 113 | optional = false 114 | python-versions = ">=3.7.0" 115 | files = [ 116 | {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, 117 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, 118 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, 119 | {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, 120 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, 121 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, 122 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, 123 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, 124 | {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, 125 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, 126 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, 127 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, 128 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, 129 | {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, 130 | {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, 131 | {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, 132 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, 133 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, 134 | {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, 135 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, 136 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, 137 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, 138 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, 139 | {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, 140 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, 141 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, 142 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, 143 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, 144 | {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, 145 | {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, 146 | {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, 147 | {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, 148 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, 149 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, 150 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, 151 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, 152 | {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, 153 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, 154 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, 155 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, 156 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, 157 | {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, 158 | {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, 159 | {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, 160 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, 161 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, 162 | {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, 163 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, 164 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, 165 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, 166 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, 167 | {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, 168 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, 169 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, 170 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, 171 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, 172 | {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, 173 | {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, 174 | {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, 175 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, 176 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, 177 | {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, 178 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, 179 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, 180 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, 181 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, 182 | {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, 183 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, 184 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, 185 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, 186 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, 187 | {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, 188 | {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, 189 | {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, 190 | {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, 191 | ] 192 | 193 | [[package]] 194 | name = "click" 195 | version = "8.1.3" 196 | description = "Composable command line interface toolkit" 197 | category = "dev" 198 | optional = false 199 | python-versions = ">=3.7" 200 | files = [ 201 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 202 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 203 | ] 204 | 205 | [package.dependencies] 206 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 207 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 208 | 209 | [[package]] 210 | name = "colorama" 211 | version = "0.4.6" 212 | description = "Cross-platform colored terminal text." 213 | category = "dev" 214 | optional = false 215 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 216 | files = [ 217 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 218 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 219 | ] 220 | 221 | [[package]] 222 | name = "coverage" 223 | version = "6.5.0" 224 | description = "Code coverage measurement for Python" 225 | category = "dev" 226 | optional = false 227 | python-versions = ">=3.7" 228 | files = [ 229 | {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, 230 | {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, 231 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, 232 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, 233 | {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"}, 234 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, 235 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, 236 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, 237 | {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, 238 | {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, 239 | {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, 240 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, 241 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, 242 | {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"}, 243 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, 244 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, 245 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, 246 | {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, 247 | {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, 248 | {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, 249 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, 250 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, 251 | {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"}, 252 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, 253 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, 254 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, 255 | {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, 256 | {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, 257 | {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, 258 | {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, 259 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, 260 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, 261 | {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"}, 262 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, 263 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, 264 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, 265 | {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, 266 | {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, 267 | {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, 268 | {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, 269 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, 270 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, 271 | {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"}, 272 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, 273 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, 274 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, 275 | {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, 276 | {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, 277 | {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, 278 | {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, 279 | ] 280 | 281 | [package.extras] 282 | toml = ["tomli"] 283 | 284 | [[package]] 285 | name = "dill" 286 | version = "0.3.6" 287 | description = "serialize all of python" 288 | category = "dev" 289 | optional = false 290 | python-versions = ">=3.7" 291 | files = [ 292 | {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, 293 | {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, 294 | ] 295 | 296 | [package.extras] 297 | graph = ["objgraph (>=1.7.2)"] 298 | 299 | [[package]] 300 | name = "distlib" 301 | version = "0.3.6" 302 | description = "Distribution utilities" 303 | category = "dev" 304 | optional = false 305 | python-versions = "*" 306 | files = [ 307 | {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, 308 | {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, 309 | ] 310 | 311 | [[package]] 312 | name = "exceptiongroup" 313 | version = "1.1.1" 314 | description = "Backport of PEP 654 (exception groups)" 315 | category = "main" 316 | optional = false 317 | python-versions = ">=3.7" 318 | files = [ 319 | {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, 320 | {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, 321 | ] 322 | 323 | [package.extras] 324 | test = ["pytest (>=6)"] 325 | 326 | [[package]] 327 | name = "fastapi" 328 | version = "0.98.0" 329 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 330 | category = "main" 331 | optional = false 332 | python-versions = ">=3.7" 333 | files = [ 334 | {file = "fastapi-0.98.0-py3-none-any.whl", hash = "sha256:f4165fb1fe3610c52cb1b8282c1480de9c34bc270f56a965aa93a884c350d605"}, 335 | {file = "fastapi-0.98.0.tar.gz", hash = "sha256:0d3c18886f652038262b5898fec6b09f4ca92ee23e9d9b1d1d24e429f84bf27b"}, 336 | ] 337 | 338 | [package.dependencies] 339 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" 340 | starlette = ">=0.27.0,<0.28.0" 341 | 342 | [package.extras] 343 | 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)"] 344 | 345 | [[package]] 346 | name = "filelock" 347 | version = "3.12.2" 348 | description = "A platform independent file lock." 349 | category = "dev" 350 | optional = false 351 | python-versions = ">=3.7" 352 | files = [ 353 | {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, 354 | {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, 355 | ] 356 | 357 | [package.extras] 358 | docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] 359 | testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] 360 | 361 | [[package]] 362 | name = "h11" 363 | version = "0.14.0" 364 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 365 | category = "dev" 366 | optional = false 367 | python-versions = ">=3.7" 368 | files = [ 369 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 370 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 371 | ] 372 | 373 | [package.dependencies] 374 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 375 | 376 | [[package]] 377 | name = "httpcore" 378 | version = "0.17.2" 379 | description = "A minimal low-level HTTP client." 380 | category = "dev" 381 | optional = false 382 | python-versions = ">=3.7" 383 | files = [ 384 | {file = "httpcore-0.17.2-py3-none-any.whl", hash = "sha256:5581b9c12379c4288fe70f43c710d16060c10080617001e6b22a3b6dbcbefd36"}, 385 | {file = "httpcore-0.17.2.tar.gz", hash = "sha256:125f8375ab60036db632f34f4b627a9ad085048eef7cb7d2616fea0f739f98af"}, 386 | ] 387 | 388 | [package.dependencies] 389 | anyio = ">=3.0,<5.0" 390 | certifi = "*" 391 | h11 = ">=0.13,<0.15" 392 | sniffio = ">=1.0.0,<2.0.0" 393 | 394 | [package.extras] 395 | http2 = ["h2 (>=3,<5)"] 396 | socks = ["socksio (>=1.0.0,<2.0.0)"] 397 | 398 | [[package]] 399 | name = "httpx" 400 | version = "0.24.1" 401 | description = "The next generation HTTP client." 402 | category = "dev" 403 | optional = false 404 | python-versions = ">=3.7" 405 | files = [ 406 | {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, 407 | {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, 408 | ] 409 | 410 | [package.dependencies] 411 | certifi = "*" 412 | httpcore = ">=0.15.0,<0.18.0" 413 | idna = "*" 414 | sniffio = "*" 415 | 416 | [package.extras] 417 | brotli = ["brotli", "brotlicffi"] 418 | cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] 419 | http2 = ["h2 (>=3,<5)"] 420 | socks = ["socksio (>=1.0.0,<2.0.0)"] 421 | 422 | [[package]] 423 | name = "identify" 424 | version = "2.5.24" 425 | description = "File identification library for Python" 426 | category = "dev" 427 | optional = false 428 | python-versions = ">=3.7" 429 | files = [ 430 | {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, 431 | {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, 432 | ] 433 | 434 | [package.extras] 435 | license = ["ukkonen"] 436 | 437 | [[package]] 438 | name = "idna" 439 | version = "3.4" 440 | description = "Internationalized Domain Names in Applications (IDNA)" 441 | category = "main" 442 | optional = false 443 | python-versions = ">=3.5" 444 | files = [ 445 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 446 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 447 | ] 448 | 449 | [[package]] 450 | name = "importlib-metadata" 451 | version = "6.7.0" 452 | description = "Read metadata from Python packages" 453 | category = "dev" 454 | optional = false 455 | python-versions = ">=3.7" 456 | files = [ 457 | {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, 458 | {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, 459 | ] 460 | 461 | [package.dependencies] 462 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 463 | zipp = ">=0.5" 464 | 465 | [package.extras] 466 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 467 | perf = ["ipython"] 468 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] 469 | 470 | [[package]] 471 | name = "iniconfig" 472 | version = "2.0.0" 473 | description = "brain-dead simple config-ini parsing" 474 | category = "dev" 475 | optional = false 476 | python-versions = ">=3.7" 477 | files = [ 478 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 479 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 480 | ] 481 | 482 | [[package]] 483 | name = "isort" 484 | version = "5.11.5" 485 | description = "A Python utility / library to sort Python imports." 486 | category = "dev" 487 | optional = false 488 | python-versions = ">=3.7.0" 489 | files = [ 490 | {file = "isort-5.11.5-py3-none-any.whl", hash = "sha256:ba1d72fb2595a01c7895a5128f9585a5cc4b6d395f1c8d514989b9a7eb2a8746"}, 491 | {file = "isort-5.11.5.tar.gz", hash = "sha256:6be1f76a507cb2ecf16c7cf14a37e41609ca082330be4e3436a18ef74add55db"}, 492 | ] 493 | 494 | [package.extras] 495 | colors = ["colorama (>=0.4.3,<0.5.0)"] 496 | pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] 497 | plugins = ["setuptools"] 498 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 499 | 500 | [[package]] 501 | name = "lazy-object-proxy" 502 | version = "1.9.0" 503 | description = "A fast and thorough lazy object proxy." 504 | category = "dev" 505 | optional = false 506 | python-versions = ">=3.7" 507 | files = [ 508 | {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, 509 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, 510 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, 511 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, 512 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, 513 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, 514 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, 515 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, 516 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, 517 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, 518 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, 519 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, 520 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, 521 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, 522 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, 523 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, 524 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, 525 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, 526 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, 527 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, 528 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, 529 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, 530 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, 531 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, 532 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, 533 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, 534 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, 535 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, 536 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, 537 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, 538 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, 539 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, 540 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, 541 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, 542 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, 543 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, 544 | ] 545 | 546 | [[package]] 547 | name = "mccabe" 548 | version = "0.7.0" 549 | description = "McCabe checker, plugin for flake8" 550 | category = "dev" 551 | optional = false 552 | python-versions = ">=3.6" 553 | files = [ 554 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 555 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 556 | ] 557 | 558 | [[package]] 559 | name = "mypy-extensions" 560 | version = "1.0.0" 561 | description = "Type system extensions for programs checked with the mypy type checker." 562 | category = "dev" 563 | optional = false 564 | python-versions = ">=3.5" 565 | files = [ 566 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 567 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 568 | ] 569 | 570 | [[package]] 571 | name = "nodeenv" 572 | version = "1.8.0" 573 | description = "Node.js virtual environment builder" 574 | category = "dev" 575 | optional = false 576 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 577 | files = [ 578 | {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, 579 | {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, 580 | ] 581 | 582 | [package.dependencies] 583 | setuptools = "*" 584 | 585 | [[package]] 586 | name = "packaging" 587 | version = "23.1" 588 | description = "Core utilities for Python packages" 589 | category = "dev" 590 | optional = false 591 | python-versions = ">=3.7" 592 | files = [ 593 | {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, 594 | {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, 595 | ] 596 | 597 | [[package]] 598 | name = "pathspec" 599 | version = "0.11.1" 600 | description = "Utility library for gitignore style pattern matching of file paths." 601 | category = "dev" 602 | optional = false 603 | python-versions = ">=3.7" 604 | files = [ 605 | {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, 606 | {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, 607 | ] 608 | 609 | [[package]] 610 | name = "platformdirs" 611 | version = "3.8.0" 612 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 613 | category = "dev" 614 | optional = false 615 | python-versions = ">=3.7" 616 | files = [ 617 | {file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"}, 618 | {file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"}, 619 | ] 620 | 621 | [package.dependencies] 622 | typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""} 623 | 624 | [package.extras] 625 | docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] 626 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] 627 | 628 | [[package]] 629 | name = "pluggy" 630 | version = "1.2.0" 631 | description = "plugin and hook calling mechanisms for python" 632 | category = "dev" 633 | optional = false 634 | python-versions = ">=3.7" 635 | files = [ 636 | {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, 637 | {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, 638 | ] 639 | 640 | [package.dependencies] 641 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 642 | 643 | [package.extras] 644 | dev = ["pre-commit", "tox"] 645 | testing = ["pytest", "pytest-benchmark"] 646 | 647 | [[package]] 648 | name = "pre-commit" 649 | version = "2.21.0" 650 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 651 | category = "dev" 652 | optional = false 653 | python-versions = ">=3.7" 654 | files = [ 655 | {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, 656 | {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, 657 | ] 658 | 659 | [package.dependencies] 660 | cfgv = ">=2.0.0" 661 | identify = ">=1.0.0" 662 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 663 | nodeenv = ">=0.11.1" 664 | pyyaml = ">=5.1" 665 | virtualenv = ">=20.10.0" 666 | 667 | [[package]] 668 | name = "pydantic" 669 | version = "1.10.9" 670 | description = "Data validation and settings management using python type hints" 671 | category = "main" 672 | optional = false 673 | python-versions = ">=3.7" 674 | files = [ 675 | {file = "pydantic-1.10.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e692dec4a40bfb40ca530e07805b1208c1de071a18d26af4a2a0d79015b352ca"}, 676 | {file = "pydantic-1.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c52eb595db83e189419bf337b59154bdcca642ee4b2a09e5d7797e41ace783f"}, 677 | {file = "pydantic-1.10.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:939328fd539b8d0edf244327398a667b6b140afd3bf7e347cf9813c736211896"}, 678 | {file = "pydantic-1.10.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b48d3d634bca23b172f47f2335c617d3fcb4b3ba18481c96b7943a4c634f5c8d"}, 679 | {file = "pydantic-1.10.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f0b7628fb8efe60fe66fd4adadd7ad2304014770cdc1f4934db41fe46cc8825f"}, 680 | {file = "pydantic-1.10.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e1aa5c2410769ca28aa9a7841b80d9d9a1c5f223928ca8bec7e7c9a34d26b1d4"}, 681 | {file = "pydantic-1.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:eec39224b2b2e861259d6f3c8b6290d4e0fbdce147adb797484a42278a1a486f"}, 682 | {file = "pydantic-1.10.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d111a21bbbfd85c17248130deac02bbd9b5e20b303338e0dbe0faa78330e37e0"}, 683 | {file = "pydantic-1.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e9aec8627a1a6823fc62fb96480abe3eb10168fd0d859ee3d3b395105ae19a7"}, 684 | {file = "pydantic-1.10.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07293ab08e7b4d3c9d7de4949a0ea571f11e4557d19ea24dd3ae0c524c0c334d"}, 685 | {file = "pydantic-1.10.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ee829b86ce984261d99ff2fd6e88f2230068d96c2a582f29583ed602ef3fc2c"}, 686 | {file = "pydantic-1.10.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4b466a23009ff5cdd7076eb56aca537c745ca491293cc38e72bf1e0e00de5b91"}, 687 | {file = "pydantic-1.10.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7847ca62e581e6088d9000f3c497267868ca2fa89432714e21a4fb33a04d52e8"}, 688 | {file = "pydantic-1.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:7845b31959468bc5b78d7b95ec52fe5be32b55d0d09983a877cca6aedc51068f"}, 689 | {file = "pydantic-1.10.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:517a681919bf880ce1dac7e5bc0c3af1e58ba118fd774da2ffcd93c5f96eaece"}, 690 | {file = "pydantic-1.10.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67195274fd27780f15c4c372f4ba9a5c02dad6d50647b917b6a92bf00b3d301a"}, 691 | {file = "pydantic-1.10.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2196c06484da2b3fded1ab6dbe182bdabeb09f6318b7fdc412609ee2b564c49a"}, 692 | {file = "pydantic-1.10.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6257bb45ad78abacda13f15bde5886efd6bf549dd71085e64b8dcf9919c38b60"}, 693 | {file = "pydantic-1.10.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3283b574b01e8dbc982080d8287c968489d25329a463b29a90d4157de4f2baaf"}, 694 | {file = "pydantic-1.10.9-cp37-cp37m-win_amd64.whl", hash = "sha256:5f8bbaf4013b9a50e8100333cc4e3fa2f81214033e05ac5aa44fa24a98670a29"}, 695 | {file = "pydantic-1.10.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9cd67fb763248cbe38f0593cd8611bfe4b8ad82acb3bdf2b0898c23415a1f82"}, 696 | {file = "pydantic-1.10.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f50e1764ce9353be67267e7fd0da08349397c7db17a562ad036aa7c8f4adfdb6"}, 697 | {file = "pydantic-1.10.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73ef93e5e1d3c8e83f1ff2e7fdd026d9e063c7e089394869a6e2985696693766"}, 698 | {file = "pydantic-1.10.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:128d9453d92e6e81e881dd7e2484e08d8b164da5507f62d06ceecf84bf2e21d3"}, 699 | {file = "pydantic-1.10.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad428e92ab68798d9326bb3e5515bc927444a3d71a93b4a2ca02a8a5d795c572"}, 700 | {file = "pydantic-1.10.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fab81a92f42d6d525dd47ced310b0c3e10c416bbfae5d59523e63ea22f82b31e"}, 701 | {file = "pydantic-1.10.9-cp38-cp38-win_amd64.whl", hash = "sha256:963671eda0b6ba6926d8fc759e3e10335e1dc1b71ff2a43ed2efd6996634dafb"}, 702 | {file = "pydantic-1.10.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:970b1bdc6243ef663ba5c7e36ac9ab1f2bfecb8ad297c9824b542d41a750b298"}, 703 | {file = "pydantic-1.10.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7e1d5290044f620f80cf1c969c542a5468f3656de47b41aa78100c5baa2b8276"}, 704 | {file = "pydantic-1.10.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83fcff3c7df7adff880622a98022626f4f6dbce6639a88a15a3ce0f96466cb60"}, 705 | {file = "pydantic-1.10.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0da48717dc9495d3a8f215e0d012599db6b8092db02acac5e0d58a65248ec5bc"}, 706 | {file = "pydantic-1.10.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0a2aabdc73c2a5960e87c3ffebca6ccde88665616d1fd6d3db3178ef427b267a"}, 707 | {file = "pydantic-1.10.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9863b9420d99dfa9c064042304868e8ba08e89081428a1c471858aa2af6f57c4"}, 708 | {file = "pydantic-1.10.9-cp39-cp39-win_amd64.whl", hash = "sha256:e7c9900b43ac14110efa977be3da28931ffc74c27e96ee89fbcaaf0b0fe338e1"}, 709 | {file = "pydantic-1.10.9-py3-none-any.whl", hash = "sha256:6cafde02f6699ce4ff643417d1a9223716ec25e228ddc3b436fe7e2d25a1f305"}, 710 | {file = "pydantic-1.10.9.tar.gz", hash = "sha256:95c70da2cd3b6ddf3b9645ecaa8d98f3d80c606624b6d245558d202cd23ea3be"}, 711 | ] 712 | 713 | [package.dependencies] 714 | typing-extensions = ">=4.2.0" 715 | 716 | [package.extras] 717 | dotenv = ["python-dotenv (>=0.10.4)"] 718 | email = ["email-validator (>=1.0.3)"] 719 | 720 | [[package]] 721 | name = "pylint" 722 | version = "2.17.4" 723 | description = "python code static checker" 724 | category = "dev" 725 | optional = false 726 | python-versions = ">=3.7.2" 727 | files = [ 728 | {file = "pylint-2.17.4-py3-none-any.whl", hash = "sha256:7a1145fb08c251bdb5cca11739722ce64a63db479283d10ce718b2460e54123c"}, 729 | {file = "pylint-2.17.4.tar.gz", hash = "sha256:5dcf1d9e19f41f38e4e85d10f511e5b9c35e1aa74251bf95cdd8cb23584e2db1"}, 730 | ] 731 | 732 | [package.dependencies] 733 | astroid = ">=2.15.4,<=2.17.0-dev0" 734 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 735 | dill = [ 736 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 737 | {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, 738 | ] 739 | isort = ">=4.2.5,<6" 740 | mccabe = ">=0.6,<0.8" 741 | platformdirs = ">=2.2.0" 742 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 743 | tomlkit = ">=0.10.1" 744 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 745 | 746 | [package.extras] 747 | spelling = ["pyenchant (>=3.2,<4.0)"] 748 | testutils = ["gitpython (>3)"] 749 | 750 | [[package]] 751 | name = "pytest" 752 | version = "7.3.2" 753 | description = "pytest: simple powerful testing with Python" 754 | category = "dev" 755 | optional = false 756 | python-versions = ">=3.7" 757 | files = [ 758 | {file = "pytest-7.3.2-py3-none-any.whl", hash = "sha256:cdcbd012c9312258922f8cd3f1b62a6580fdced17db6014896053d47cddf9295"}, 759 | {file = "pytest-7.3.2.tar.gz", hash = "sha256:ee990a3cc55ba808b80795a79944756f315c67c12b56abd3ac993a7b8c17030b"}, 760 | ] 761 | 762 | [package.dependencies] 763 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 764 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 765 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 766 | iniconfig = "*" 767 | packaging = "*" 768 | pluggy = ">=0.12,<2.0" 769 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 770 | 771 | [package.extras] 772 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 773 | 774 | [[package]] 775 | name = "pyyaml" 776 | version = "6.0" 777 | description = "YAML parser and emitter for Python" 778 | category = "dev" 779 | optional = false 780 | python-versions = ">=3.6" 781 | files = [ 782 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 783 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 784 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 785 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 786 | {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"}, 787 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 788 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 789 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, 790 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, 791 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, 792 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, 793 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, 794 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, 795 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, 796 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 797 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 798 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 799 | {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"}, 800 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 801 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 802 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 803 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 804 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 805 | {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"}, 806 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 807 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 808 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 809 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 810 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 811 | {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"}, 812 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 813 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 814 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 815 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 816 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 817 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 818 | {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"}, 819 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 820 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 821 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 822 | ] 823 | 824 | [[package]] 825 | name = "requests" 826 | version = "2.31.0" 827 | description = "Python HTTP for Humans." 828 | category = "dev" 829 | optional = false 830 | python-versions = ">=3.7" 831 | files = [ 832 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 833 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 834 | ] 835 | 836 | [package.dependencies] 837 | certifi = ">=2017.4.17" 838 | charset-normalizer = ">=2,<4" 839 | idna = ">=2.5,<4" 840 | urllib3 = ">=1.21.1,<3" 841 | 842 | [package.extras] 843 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 844 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 845 | 846 | [[package]] 847 | name = "ruff" 848 | version = "0.0.275" 849 | description = "An extremely fast Python linter, written in Rust." 850 | category = "dev" 851 | optional = false 852 | python-versions = ">=3.7" 853 | files = [ 854 | {file = "ruff-0.0.275-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:5e6554a072e7ce81eb6f0bec1cebd3dcb0e358652c0f4900d7d630d61691e914"}, 855 | {file = "ruff-0.0.275-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:1cc599022fe5ffb143a965b8d659eb64161ab8ab4433d208777eab018a1aab67"}, 856 | {file = "ruff-0.0.275-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5206fc1cd8c1c1deadd2e6360c0dbcd690f1c845da588ca9d32e4a764a402c60"}, 857 | {file = "ruff-0.0.275-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c4e6468da26f77b90cae35319d310999f471a8c352998e9b39937a23750149e"}, 858 | {file = "ruff-0.0.275-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0dbdea02942131dbc15dd45f431d152224f15e1dd1859fcd0c0487b658f60f1a"}, 859 | {file = "ruff-0.0.275-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:22efd9f41af27ef8fb9779462c46c35c89134d33e326c889971e10b2eaf50c63"}, 860 | {file = "ruff-0.0.275-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c09662112cfa22d7467a19252a546291fd0eae4f423e52b75a7a2000a1894db"}, 861 | {file = "ruff-0.0.275-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80043726662144876a381efaab88841c88e8df8baa69559f96b22d4fa216bef1"}, 862 | {file = "ruff-0.0.275-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5859ee543b01b7eb67835dfd505faa8bb7cc1550f0295c92c1401b45b42be399"}, 863 | {file = "ruff-0.0.275-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c8ace4d40a57b5ea3c16555f25a6b16bc5d8b2779ae1912ce2633543d4e9b1da"}, 864 | {file = "ruff-0.0.275-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8347fc16aa185aae275906c4ac5b770e00c896b6a0acd5ba521f158801911998"}, 865 | {file = "ruff-0.0.275-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ec43658c64bfda44fd84bbea9da8c7a3b34f65448192d1c4dd63e9f4e7abfdd4"}, 866 | {file = "ruff-0.0.275-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:508b13f7ca37274cceaba4fb3ea5da6ca192356323d92acf39462337c33ad14e"}, 867 | {file = "ruff-0.0.275-py3-none-win32.whl", hash = "sha256:6afb1c4422f24f361e877937e2a44b3f8176774a476f5e33845ebfe887dd5ec2"}, 868 | {file = "ruff-0.0.275-py3-none-win_amd64.whl", hash = "sha256:d9b264d78621bf7b698b6755d4913ab52c19bd28bee1a16001f954d64c1a1220"}, 869 | {file = "ruff-0.0.275-py3-none-win_arm64.whl", hash = "sha256:a19ce3bea71023eee5f0f089dde4a4272d088d5ac0b675867e074983238ccc65"}, 870 | {file = "ruff-0.0.275.tar.gz", hash = "sha256:a63a0b645da699ae5c758fce19188e901b3033ec54d862d93fcd042addf7f38d"}, 871 | ] 872 | 873 | [[package]] 874 | name = "setuptools" 875 | version = "68.0.0" 876 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 877 | category = "dev" 878 | optional = false 879 | python-versions = ">=3.7" 880 | files = [ 881 | {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, 882 | {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, 883 | ] 884 | 885 | [package.extras] 886 | 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"] 887 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "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-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 888 | 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"] 889 | 890 | [[package]] 891 | name = "sniffio" 892 | version = "1.3.0" 893 | description = "Sniff out which async library your code is running under" 894 | category = "main" 895 | optional = false 896 | python-versions = ">=3.7" 897 | files = [ 898 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 899 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 900 | ] 901 | 902 | [[package]] 903 | name = "starlette" 904 | version = "0.27.0" 905 | description = "The little ASGI library that shines." 906 | category = "main" 907 | optional = false 908 | python-versions = ">=3.7" 909 | files = [ 910 | {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, 911 | {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, 912 | ] 913 | 914 | [package.dependencies] 915 | anyio = ">=3.4.0,<5" 916 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 917 | 918 | [package.extras] 919 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] 920 | 921 | [[package]] 922 | name = "tomli" 923 | version = "2.0.1" 924 | description = "A lil' TOML parser" 925 | category = "dev" 926 | optional = false 927 | python-versions = ">=3.7" 928 | files = [ 929 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 930 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 931 | ] 932 | 933 | [[package]] 934 | name = "tomlkit" 935 | version = "0.11.8" 936 | description = "Style preserving TOML library" 937 | category = "dev" 938 | optional = false 939 | python-versions = ">=3.7" 940 | files = [ 941 | {file = "tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171"}, 942 | {file = "tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3"}, 943 | ] 944 | 945 | [[package]] 946 | name = "typed-ast" 947 | version = "1.5.4" 948 | description = "a fork of Python 2 and 3 ast modules with type comment support" 949 | category = "dev" 950 | optional = false 951 | python-versions = ">=3.6" 952 | files = [ 953 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, 954 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, 955 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, 956 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, 957 | {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, 958 | {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, 959 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, 960 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, 961 | {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, 962 | {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, 963 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, 964 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, 965 | {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, 966 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, 967 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, 968 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, 969 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, 970 | {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, 971 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, 972 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, 973 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, 974 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, 975 | {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, 976 | {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, 977 | ] 978 | 979 | [[package]] 980 | name = "typing-extensions" 981 | version = "4.6.3" 982 | description = "Backported and Experimental Type Hints for Python 3.7+" 983 | category = "main" 984 | optional = false 985 | python-versions = ">=3.7" 986 | files = [ 987 | {file = "typing_extensions-4.6.3-py3-none-any.whl", hash = "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26"}, 988 | {file = "typing_extensions-4.6.3.tar.gz", hash = "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"}, 989 | ] 990 | 991 | [[package]] 992 | name = "urllib3" 993 | version = "2.0.3" 994 | description = "HTTP library with thread-safe connection pooling, file post, and more." 995 | category = "main" 996 | optional = false 997 | python-versions = ">=3.7" 998 | files = [ 999 | {file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"}, 1000 | {file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"}, 1001 | ] 1002 | 1003 | [package.extras] 1004 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1005 | secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] 1006 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1007 | zstd = ["zstandard (>=0.18.0)"] 1008 | 1009 | [[package]] 1010 | name = "virtualenv" 1011 | version = "20.23.1" 1012 | description = "Virtual Python Environment builder" 1013 | category = "dev" 1014 | optional = false 1015 | python-versions = ">=3.7" 1016 | files = [ 1017 | {file = "virtualenv-20.23.1-py3-none-any.whl", hash = "sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419"}, 1018 | {file = "virtualenv-20.23.1.tar.gz", hash = "sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1"}, 1019 | ] 1020 | 1021 | [package.dependencies] 1022 | distlib = ">=0.3.6,<1" 1023 | filelock = ">=3.12,<4" 1024 | importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} 1025 | platformdirs = ">=3.5.1,<4" 1026 | 1027 | [package.extras] 1028 | docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1029 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"] 1030 | 1031 | [[package]] 1032 | name = "wrapt" 1033 | version = "1.15.0" 1034 | description = "Module for decorators, wrappers and monkey patching." 1035 | category = "dev" 1036 | optional = false 1037 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 1038 | files = [ 1039 | {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, 1040 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, 1041 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, 1042 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, 1043 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, 1044 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, 1045 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, 1046 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, 1047 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, 1048 | {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, 1049 | {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, 1050 | {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, 1051 | {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, 1052 | {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"}, 1053 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, 1054 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, 1055 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, 1056 | {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, 1057 | {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, 1058 | {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, 1059 | {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, 1060 | {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, 1061 | {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, 1062 | {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"}, 1063 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, 1064 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, 1065 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, 1066 | {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, 1067 | {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, 1068 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, 1069 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, 1070 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, 1071 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, 1072 | {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, 1073 | {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, 1074 | {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, 1075 | {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, 1076 | {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, 1077 | {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"}, 1078 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, 1079 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, 1080 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, 1081 | {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, 1082 | {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, 1083 | {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, 1084 | {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, 1085 | {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, 1086 | {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"}, 1087 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, 1088 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, 1089 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, 1090 | {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, 1091 | {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, 1092 | {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, 1093 | {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, 1094 | {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, 1095 | {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, 1096 | {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"}, 1097 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, 1098 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, 1099 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, 1100 | {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, 1101 | {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, 1102 | {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, 1103 | {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, 1104 | {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, 1105 | {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, 1106 | {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"}, 1107 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, 1108 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, 1109 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, 1110 | {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, 1111 | {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, 1112 | {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, 1113 | {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "zipp" 1118 | version = "3.15.0" 1119 | description = "Backport of pathlib-compatible object wrapper for zip files" 1120 | category = "dev" 1121 | optional = false 1122 | python-versions = ">=3.7" 1123 | files = [ 1124 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 1125 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 1126 | ] 1127 | 1128 | [package.extras] 1129 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 1130 | 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)"] 1131 | 1132 | [metadata] 1133 | lock-version = "2.0" 1134 | python-versions = ">=3.7.2,<4" 1135 | content-hash = "d292b54e797c4d23879450e6db81a0d8a179eecd72e1ba012372ebc50c1dbaf5" 1136 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | # This Pylint rcfile contains a best-effort configuration to uphold the 2 | # best-practices and style described in the Google Python style guide: 3 | # https://google.github.io/styleguide/pyguide.html 4 | # 5 | # Its canonical open-source location is: 6 | # https://google.github.io/styleguide/pylintrc 7 | 8 | [MASTER] 9 | 10 | # Files or directories to be skipped. They should be base names, not paths. 11 | ignore=third_party 12 | 13 | # Files or directories matching the regex patterns are skipped. The regex 14 | # matches against base names, not paths. 15 | ignore-patterns= 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=no 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | # Use multiple processes to speed up Pylint. 25 | jobs=4 26 | 27 | # Allow loading of arbitrary C extensions. Extensions are imported into the 28 | # active Python interpreter and may run arbitrary code. 29 | unsafe-load-any-extension=no 30 | 31 | 32 | [MESSAGES CONTROL] 33 | 34 | # Only show warnings with the listed confidence levels. Leave empty to show 35 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 36 | confidence= 37 | 38 | # Enable the message, report, category or checker with the given id(s). You can 39 | # either give multiple identifier separated by comma (,) or put this option 40 | # multiple time (only on the command line, not in the configuration file where 41 | # it should appear only once). See also the "--disable" option for examples. 42 | #enable= 43 | 44 | # Disable the message, report, category or checker with the given id(s). You 45 | # can either give multiple identifiers separated by comma (,) or put this 46 | # option multiple times (only on the command line, not in the configuration 47 | # file where it should appear only once).You can also use "--disable=all" to 48 | # disable everything first and then reenable specific checks. For example, if 49 | # you want to run only the similarities checker, you can use "--disable=all 50 | # --enable=similarities". If you want to run only the classes checker, but have 51 | # no Warning level messages displayed, use"--disable=all --enable=classes 52 | # --disable=W" 53 | disable=abstract-method, 54 | apply-builtin, 55 | arguments-differ, 56 | attribute-defined-outside-init, 57 | backtick, 58 | bad-option-value, 59 | basestring-builtin, 60 | buffer-builtin, 61 | c-extension-no-member, 62 | consider-using-enumerate, 63 | cmp-builtin, 64 | cmp-method, 65 | coerce-builtin, 66 | coerce-method, 67 | delslice-method, 68 | div-method, 69 | duplicate-code, 70 | eq-without-hash, 71 | execfile-builtin, 72 | file-builtin, 73 | filter-builtin-not-iterating, 74 | fixme, 75 | getslice-method, 76 | global-statement, 77 | hex-method, 78 | idiv-method, 79 | implicit-str-concat, 80 | import-error, 81 | import-self, 82 | import-star-module-level, 83 | inconsistent-return-statements, 84 | input-builtin, 85 | intern-builtin, 86 | invalid-str-codec, 87 | locally-disabled, 88 | long-builtin, 89 | long-suffix, 90 | map-builtin-not-iterating, 91 | misplaced-comparison-constant, 92 | missing-function-docstring, 93 | metaclass-assignment, 94 | next-method-called, 95 | next-method-defined, 96 | no-absolute-import, 97 | no-else-break, 98 | no-else-continue, 99 | no-else-raise, 100 | no-else-return, 101 | no-init, # added 102 | no-member, 103 | no-name-in-module, 104 | no-self-use, 105 | nonzero-method, 106 | oct-method, 107 | old-division, 108 | old-ne-operator, 109 | old-octal-literal, 110 | old-raise-syntax, 111 | parameter-unpacking, 112 | print-statement, 113 | raising-string, 114 | range-builtin-not-iterating, 115 | raw_input-builtin, 116 | rdiv-method, 117 | reduce-builtin, 118 | relative-import, 119 | reload-builtin, 120 | round-builtin, 121 | setslice-method, 122 | signature-differs, 123 | standarderror-builtin, 124 | suppressed-message, 125 | sys-max-int, 126 | too-few-public-methods, 127 | too-many-ancestors, 128 | too-many-arguments, 129 | too-many-boolean-expressions, 130 | too-many-branches, 131 | too-many-instance-attributes, 132 | too-many-locals, 133 | too-many-nested-blocks, 134 | too-many-public-methods, 135 | too-many-return-statements, 136 | too-many-statements, 137 | trailing-newlines, 138 | unichr-builtin, 139 | unicode-builtin, 140 | unnecessary-pass, 141 | unpacking-in-except, 142 | useless-else-on-loop, 143 | useless-object-inheritance, 144 | useless-suppression, 145 | using-cmp-argument, 146 | wrong-import-order, 147 | xrange-builtin, 148 | zip-builtin-not-iterating, 149 | 150 | 151 | [REPORTS] 152 | 153 | # Set the output format. Available formats are text, parseable, colorized, msvs 154 | # (visual studio) and html. You can also give a reporter class, eg 155 | # mypackage.mymodule.MyReporterClass. 156 | output-format=text 157 | 158 | # Tells whether to display a full report or only the messages 159 | reports=no 160 | 161 | # Python expression which should return a note less than 10 (10 is the highest 162 | # note). You have access to the variables errors warning, statement which 163 | # respectively contain the number of errors / warnings messages and the total 164 | # number of statements analyzed. This is used by the global evaluation report 165 | # (RP0004). 166 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 167 | 168 | # Template used to display messages. This is a python new-style format string 169 | # used to format the message information. See doc for all details 170 | #msg-template= 171 | 172 | 173 | [BASIC] 174 | 175 | # Good variable names which should always be accepted, separated by a comma 176 | good-names=main,_ 177 | 178 | # Bad variable names which should always be refused, separated by a comma 179 | bad-names= 180 | 181 | # Colon-delimited sets of names that determine each other's naming style when 182 | # the name regexes allow several styles. 183 | name-group= 184 | 185 | # Include a hint for the correct naming format with invalid-name 186 | include-naming-hint=no 187 | 188 | # List of decorators that produce properties, such as abc.abstractproperty. Add 189 | # to this list to register other decorators that produce valid properties. 190 | property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl 191 | 192 | # Regular expression matching correct function names 193 | function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ 194 | 195 | # Regular expression matching correct variable names 196 | variable-rgx=^[a-z][a-z0-9_]*$ 197 | 198 | # Regular expression matching correct constant names 199 | const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ 200 | 201 | # Regular expression matching correct attribute names 202 | attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ 203 | 204 | # Regular expression matching correct argument names 205 | argument-rgx=^[a-z][a-z0-9_]*$ 206 | 207 | # Regular expression matching correct class attribute names 208 | class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ 209 | 210 | # Regular expression matching correct inline iteration names 211 | inlinevar-rgx=^[a-z][a-z0-9_]*$ 212 | 213 | # Regular expression matching correct class names 214 | class-rgx=^_?[A-Z][a-zA-Z0-9]*$ 215 | 216 | # Regular expression matching correct module names 217 | module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ 218 | 219 | # Regular expression matching correct method names 220 | method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ 221 | 222 | # Regular expression which should only match function or class names that do 223 | # not require a docstring. 224 | no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ 225 | 226 | # Minimum line length for functions/classes that require docstrings, shorter 227 | # ones are exempt. 228 | docstring-min-length=10 229 | 230 | 231 | [TYPECHECK] 232 | 233 | # List of decorators that produce context managers, such as 234 | # contextlib.contextmanager. Add to this list to register other decorators that 235 | # produce valid context managers. 236 | contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager 237 | 238 | # Tells whether missing members accessed in mixin class should be ignored. A 239 | # mixin class is detected if its name ends with "mixin" (case insensitive). 240 | ignore-mixin-members=yes 241 | 242 | # List of module names for which member attributes should not be checked 243 | # (useful for modules/projects where namespaces are manipulated during runtime 244 | # and thus existing member attributes cannot be deduced by static analysis. It 245 | # supports qualified module names, as well as Unix pattern matching. 246 | ignored-modules= 247 | 248 | # List of class names for which member attributes should not be checked (useful 249 | # for classes with dynamically set attributes). This supports the use of 250 | # qualified names. 251 | ignored-classes=optparse.Values,thread._local,_thread._local 252 | 253 | # List of members which are set dynamically and missed by pylint inference 254 | # system, and so shouldn't trigger E1101 when accessed. Python regular 255 | # expressions are accepted. 256 | generated-members= 257 | 258 | 259 | [FORMAT] 260 | 261 | # Maximum number of characters on a single line. 262 | max-line-length=90 263 | 264 | # TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt 265 | # lines made too long by directives to pytype. 266 | 267 | # Regexp for a line that is allowed to be longer than the limit. 268 | ignore-long-lines=(?x)( 269 | ^\s*(\#\ )??$| 270 | ^\s*(from\s+\S+\s+)?import\s+.+$) 271 | 272 | # Allow the body of an if to be on the same line as the test if there is no 273 | # else. 274 | single-line-if-stmt=yes 275 | 276 | # Maximum number of lines in a module 277 | max-module-lines=99999 278 | 279 | # String used as indentation unit. The internal Google style guide mandates 2 280 | # spaces. Google's externaly-published style guide says 4, consistent with 281 | # PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google 282 | # projects (like TensorFlow). 283 | indent-string=' ' 284 | 285 | # Number of spaces of indent required inside a hanging or continued line. 286 | indent-after-paren=4 287 | 288 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 289 | expected-line-ending-format= 290 | 291 | 292 | [MISCELLANEOUS] 293 | 294 | # List of note tags to take in consideration, separated by a comma. 295 | notes=TODO 296 | 297 | 298 | [STRING] 299 | 300 | # This flag controls whether inconsistent-quotes generates a warning when the 301 | # character used as a quote delimiter is used inconsistently within a module. 302 | check-quote-consistency=yes 303 | 304 | 305 | [VARIABLES] 306 | 307 | # Tells whether we should check for unused import in __init__ files. 308 | init-import=no 309 | 310 | # A regular expression matching the name of dummy variables (i.e. expectedly 311 | # not used). 312 | dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) 313 | 314 | # List of additional names supposed to be defined in builtins. Remember that 315 | # you should avoid to define new builtins when possible. 316 | additional-builtins= 317 | 318 | # List of strings which can identify a callback function by name. A callback 319 | # name must start or end with one of those strings. 320 | callbacks=cb_,_cb 321 | 322 | # List of qualified module names which can have objects that can redefine 323 | # builtins. 324 | redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools 325 | 326 | 327 | [LOGGING] 328 | 329 | # Logging modules to check that the string format arguments are in logging 330 | # function parameter format 331 | logging-modules=logging,absl.logging,tensorflow.io.logging 332 | 333 | 334 | [SIMILARITIES] 335 | 336 | # Minimum lines number of a similarity. 337 | min-similarity-lines=4 338 | 339 | # Ignore comments when computing similarities. 340 | ignore-comments=yes 341 | 342 | # Ignore docstrings when computing similarities. 343 | ignore-docstrings=yes 344 | 345 | # Ignore imports when computing similarities. 346 | ignore-imports=no 347 | 348 | 349 | [SPELLING] 350 | 351 | # Spelling dictionary name. Available dictionaries: none. To make it working 352 | # install python-enchant package. 353 | spelling-dict= 354 | 355 | # List of comma separated words that should not be checked. 356 | spelling-ignore-words= 357 | 358 | # A path to a file that contains private dictionary; one word per line. 359 | spelling-private-dict-file= 360 | 361 | # Tells whether to store unknown words to indicated private dictionary in 362 | # --spelling-private-dict-file option instead of raising a message. 363 | spelling-store-unknown-words=no 364 | 365 | 366 | [IMPORTS] 367 | 368 | # Deprecated modules which should not be used, separated by a comma 369 | deprecated-modules=regsub, 370 | TERMIOS, 371 | Bastion, 372 | rexec, 373 | sets 374 | 375 | # Create a graph of every (i.e. internal and external) dependencies in the 376 | # given file (report RP0402 must not be disabled) 377 | import-graph= 378 | 379 | # Create a graph of external dependencies in the given file (report RP0402 must 380 | # not be disabled) 381 | ext-import-graph= 382 | 383 | # Create a graph of internal dependencies in the given file (report RP0402 must 384 | # not be disabled) 385 | int-import-graph= 386 | 387 | # Force import order to recognize a module as part of the standard 388 | # compatibility libraries. 389 | known-standard-library= 390 | 391 | # Force import order to recognize a module as part of a third party library. 392 | known-third-party=enchant, absl 393 | 394 | # Analyse import fallback blocks. This can be used to support both Python 2 and 395 | # 3 compatible code, which means that the block might have code that exists 396 | # only in one or another interpreter, leading to false positives when analysed. 397 | analyse-fallback-blocks=no 398 | 399 | 400 | [CLASSES] 401 | 402 | # List of method names used to declare (i.e. assign) instance attributes. 403 | defining-attr-methods=__init__, 404 | __new__, 405 | setUp 406 | 407 | # List of member names, which should be excluded from the protected access 408 | # warning. 409 | exclude-protected=_asdict, 410 | _fields, 411 | _replace, 412 | _source, 413 | _make 414 | 415 | # List of valid names for the first argument in a class method. 416 | valid-classmethod-first-arg=cls, 417 | class_ 418 | 419 | # List of valid names for the first argument in a metaclass class method. 420 | valid-metaclass-classmethod-first-arg=mcs 421 | 422 | 423 | [EXCEPTIONS] 424 | 425 | # Exceptions that will emit a warning when being caught. Defaults to 426 | # "Exception" 427 | overgeneral-exceptions=StandardError, 428 | Exception, 429 | BaseException 430 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fastapi_simple_security" 3 | version = "1.3.0" 4 | description = "API key-based security for FastAPI" 5 | authors = ["mrtolkien "] 6 | license = "MIT" 7 | repository = "https://github.com/mrtolkien/fastapi_simple_security" 8 | readme = "README.md" 9 | 10 | [tool.poetry.dependencies] 11 | python = ">=3.7.2,<4" 12 | # 0.70 is needed to have the TestClient object 13 | fastapi = ">=0.70" 14 | # Necessary as older versions don't work on python 3.10 15 | urllib3 = ">=1.26.12" 16 | 17 | [tool.poetry.group.dev.dependencies] 18 | pytest = "^7.0.0" 19 | httpx = "^0.24.1" 20 | requests = "^2.26.0" 21 | coverage = "^6.5.0" 22 | black = "^22.3.0" 23 | ruff = "^0.0.275" 24 | 25 | [build-system] 26 | requires = ["poetry-core>=1.0.0"] 27 | build-backend = "poetry.core.masonry.api" 28 | 29 | [tool.isort] 30 | profile = "black" 31 | 32 | [tool.coverage.report] 33 | omit = ["tests/conftest.py"] 34 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Pytest configuration. 2 | """ 3 | import os 4 | import time 5 | 6 | import pytest 7 | from fastapi.testclient import TestClient 8 | 9 | from app.main import app 10 | from fastapi_simple_security._sqlite_access import sqlite_access 11 | 12 | # The environment variable needs to be set before importing app 13 | admin_key_value = "secret" 14 | os.environ["FASTAPI_SIMPLE_SECURITY_SECRET"] = admin_key_value 15 | 16 | 17 | @pytest.fixture 18 | def admin_key(): 19 | return admin_key_value 20 | 21 | 22 | @pytest.fixture 23 | def client(): 24 | try: 25 | # We remove the existing db file 26 | os.remove("sqlite.db") 27 | # We had disk I/O errors without this 28 | time.sleep(0.1) 29 | # We re-create the file and tables 30 | sqlite_access.init_db() 31 | 32 | except FileNotFoundError: 33 | pass 34 | 35 | return TestClient(app) 36 | -------------------------------------------------------------------------------- /tests/test_functional.py: -------------------------------------------------------------------------------- 1 | """Basic unit testing. 2 | """ 3 | import sqlite3 4 | 5 | from fastapi.testclient import TestClient 6 | 7 | from fastapi_simple_security._sqlite_access import sqlite_access 8 | 9 | # TODO Rename test files and group them properly (db tests, endpoints tests, ...) 10 | 11 | 12 | def test_database_migration(): 13 | # Emulate old db 14 | with sqlite3.connect(sqlite_access.db_location) as connection: 15 | c = connection.cursor() 16 | # Create database 17 | c.execute( 18 | """ 19 | CREATE TABLE IF NOT EXISTS fastapi_simple_security ( 20 | api_key TEXT PRIMARY KEY, 21 | is_active INTEGER, 22 | never_expire INTEGER, 23 | expiration_date TEXT, 24 | latest_query_date TEXT, 25 | total_queries INTEGER) 26 | """ 27 | ) 28 | connection.commit() 29 | 30 | # Apply migration 31 | sqlite_access.init_db() 32 | 33 | # Test of the migration execution 34 | with sqlite3.connect(sqlite_access.db_location) as connection: 35 | c = connection.cursor() 36 | c.execute("PRAGMA table_info(fastapi_simple_security);") 37 | columns = c.fetchall() 38 | assert len(columns) == 7, columns # Colum 'name' created 39 | 40 | 41 | def test_api_key_name(client: TestClient, admin_key: str): 42 | response = client.get( 43 | url="/auth/new", headers={"secret-key": admin_key}, params={"name": "Test"} 44 | ) 45 | assert response.status_code == 200 46 | 47 | response = client.get("/auth/logs", headers={"secret-key": admin_key}) 48 | assert response.status_code == 200 49 | assert len(response.json()["logs"]) == 1 50 | api_key_infos = response.json()["logs"][0] 51 | assert api_key_infos.get("name") == "Test", api_key_infos 52 | 53 | 54 | def test_api_key_never_expire(client: TestClient, admin_key: str): 55 | # Create with never_expires param 56 | response = client.get( 57 | url="/auth/new", 58 | headers={"secret-key": admin_key}, 59 | params={"never_expires": True}, 60 | ) 61 | assert response.status_code == 200 62 | 63 | response = client.get("/auth/logs", headers={"secret-key": admin_key}) 64 | assert response.status_code == 200 65 | assert len(response.json()["logs"]) == 1 66 | api_key_infos = response.json()["logs"][0] 67 | assert api_key_infos.get("never_expire") is True, api_key_infos 68 | -------------------------------------------------------------------------------- /tests/test_security.py: -------------------------------------------------------------------------------- 1 | """Basic unit testing. 2 | """ 3 | import os 4 | 5 | from fastapi.testclient import TestClient 6 | 7 | 8 | def test_no_api_key(client: TestClient): 9 | response = client.get("/unsecured") 10 | assert response.status_code == 200 11 | 12 | response = client.get("/secure") 13 | assert response.status_code == 403 14 | 15 | 16 | def get_api_key(client: TestClient, admin_key: str): 17 | response = client.get(url="/auth/new", headers={"secret-key": admin_key}) 18 | assert response.status_code == 200 19 | 20 | return response.json() 21 | 22 | 23 | def test_get_api_key(client: TestClient, admin_key: str): 24 | get_api_key(client, admin_key) 25 | 26 | 27 | def test_query(client: TestClient, admin_key: str): 28 | api_key = get_api_key(client, admin_key) 29 | 30 | response = client.get(url=f"/secure?api-key={api_key}") 31 | assert response.status_code == 200 32 | 33 | 34 | def test_header(client: TestClient, admin_key: str): 35 | api_key = get_api_key(client, admin_key) 36 | 37 | response = client.get("/secure", headers={"api-key": api_key}) 38 | assert response.status_code == 200 39 | 40 | 41 | def test_revoke(client: TestClient, admin_key: str): 42 | api_key = get_api_key(client, admin_key) 43 | 44 | response = client.get("/secure", headers={"api-key": api_key}) 45 | assert response.status_code == 200 46 | 47 | response = client.get( 48 | f"/auth/revoke?api-key={api_key}", 49 | headers={"secret-key": admin_key}, 50 | ) 51 | assert response.status_code == 200 52 | 53 | response = client.get("/secure", headers={"api-key": api_key}) 54 | assert response.status_code == 403 55 | 56 | 57 | def test_renew_basic(client: TestClient, admin_key: str): 58 | api_key = get_api_key(client, admin_key) 59 | 60 | response = client.get( 61 | f"/auth/renew?api-key={api_key}", 62 | headers={"secret-key": admin_key}, 63 | ) 64 | 65 | assert response.status_code == 200 66 | 67 | 68 | def test_renew_custom_date(client: TestClient, admin_key: str): 69 | api_key = get_api_key(client, admin_key) 70 | 71 | response = client.get( 72 | f"/auth/renew?api-key={api_key}&expiration-date=2222-01-01", 73 | headers={"secret-key": admin_key}, 74 | ) 75 | 76 | assert response.status_code == 200 77 | 78 | 79 | def test_renew_wrong_date(client: TestClient, admin_key: str): 80 | api_key = get_api_key(client, admin_key) 81 | 82 | response = client.get( 83 | f"/auth/renew?api-key={api_key}&expiration-date=2222-22-22", 84 | headers={"secret-key": admin_key}, 85 | ) 86 | 87 | assert response.status_code == 422 88 | 89 | 90 | def test_renew_wrong_key(client: TestClient, admin_key: str): 91 | response = client.get( 92 | "/auth/renew?api-key=123456", 93 | headers={"secret-key": admin_key}, 94 | ) 95 | 96 | assert response.status_code == 404 97 | 98 | 99 | def test_renew_revoked(client: TestClient, admin_key: str): 100 | api_key = get_api_key(client, admin_key) 101 | 102 | response = client.get( 103 | f"/auth/revoke?api-key={api_key}", 104 | headers={"secret-key": admin_key}, 105 | ) 106 | 107 | response = client.get( 108 | f"/auth/renew?api-key={api_key}", 109 | headers={"secret-key": admin_key}, 110 | ) 111 | 112 | assert response.status_code == 200 113 | assert "This API key was revoked and has been reactivated." in response.json() 114 | 115 | 116 | def test_get_usage_stats(client: TestClient, admin_key: str): 117 | api_key = get_api_key(client, admin_key) 118 | 119 | for _ in range(5): 120 | client.get(url=f"/secure?api-key={api_key}") 121 | 122 | response = client.get("/auth/logs", headers={"secret-key": admin_key}) 123 | 124 | assert response.status_code == 200 125 | 126 | assert response.json()["logs"][0]["total_queries"] == 5 127 | 128 | 129 | def test_no_admin_key(client: TestClient): 130 | response = client.get(url="/auth/new") 131 | assert response.status_code == 403 132 | 133 | 134 | def test_wrong_admin_key(client: TestClient): 135 | response = client.get(url="/auth/new", headers={"secret-key": "WRONG_SECRET_KEY"}) 136 | assert response.status_code == 403 137 | 138 | 139 | def test_no_secret(): 140 | del os.environ["FASTAPI_SIMPLE_SECURITY_SECRET"] 141 | 142 | from fastapi_simple_security._security_secret import secret 143 | 144 | value = secret.get_secret_value() 145 | 146 | assert value != "secret" 147 | assert len(value) == 36 148 | --------------------------------------------------------------------------------