From 35dae98e87d1a3fb0bb077e8264a5b94d47f4089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Piku=C5=82a?= Date: Sun, 4 Jun 2023 15:56:29 +0000 Subject: [PATCH] Revert "Reverting to 0.6.1" This reverts commit 2033f802589e32f9628b4732a14ea898cfd568a7. All the changes were introduced in #86. Reference it for full history. --- .devcontainer/devcontainer.json | 47 + .github/workflows/lint.yml | 39 + .gitignore | 181 ++- .pre-commit-config.yaml | 39 + .vscode/launch.json | 39 + .vscode/settings.json | 8 + Dockerfile | 6 +- Jenkinsfile | 15 +- README.md | 2 + auth.py | 242 ++++ config.py | 545 ++++++++ headscale.py | 512 ++----- helper.py | 420 ++---- poetry.lock | 2267 +++++++++++++++++++++++++++++++ poetry.toml | 2 + pyproject.toml | 39 +- renderer.py | 1591 +++++++++++++--------- server.py | 879 +++++------- static/js/custom.js | 322 +++-- templates/error.html | 10 +- templates/machines.html | 16 +- templates/overview.html | 6 +- templates/routes.html | 8 +- templates/settings.html | 15 +- templates/template.html | 18 +- templates/users.html | 12 +- 26 files changed, 5190 insertions(+), 2090 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/lint.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 auth.py create mode 100644 config.py create mode 100644 poetry.lock create mode 100644 poetry.toml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..e375adb --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,47 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "Headscale WebUI", + "image": "mcr.microsoft.com/devcontainers/python:0-3", + "features": { + "ghcr.io/meaningful-ooo/devcontainer-features/fish:1": {}, + "ghcr.io/devcontainers-contrib/features/poetry:2": { + "version": "latest" + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "bungcip.better-toml", + "charliermarsh.ruff", + "github.vscode-github-actions", + "GitHub.vscode-pull-request-github", + "mhutchie.git-graph", + "ms-azuretools.vscode-docker", + "njpwerner.autodocstring", + "redhat.vscode-yaml", + "streetsidesoftware.code-spell-checker" + ], + "settings": { + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.venvPath": "${workspaceFolder}/.venv", + "python.formatting.blackPath": "${workspaceFolder}/.venv/bin/black", + "python.linting.mypyPath": "${workspaceFolder}/.venv/bin/mypy", + "python.linting.mypyEnabled": true, + "python.linting.pylintPath": "${workspaceFolder}/.venv/bin/pylint", + "python.linting.pylintEnabled": true, + } + } + } + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..5d4fdf6 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,39 @@ +name: build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + release: + types: + - published + schedule: + - cron: "0 0 * * 0" + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + tool: + - "black --check --diff" + - "isort --check --diff" + - "pydocstyle" + - "pylint --disable=fixme" + - "ruff" + - "mypy" + steps: + - uses: actions/checkout@v3 + - name: Install Poetry + run: pipx install poetry + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + cache: "poetry" + - name: Install dependencies + run: poetry install --only main,dev + - name: Run formatter + run: poetry run ${{ matrix.tool }} *.py diff --git a/.gitignore b/.gitignore index 29e1466..9143f53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,179 @@ -__pycache__ -.venv +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +coverage.lcov +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env -poetry.lock +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +# poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python + +data/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..09f3253 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +repos: + - repo: https://github.com/python-poetry/poetry + rev: "1.4.0" + hooks: + - id: poetry-check + - repo: https://github.com/ambv/black + rev: 23.3.0 + hooks: + - id: black + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + - repo: https://github.com/pycqa/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle + - repo: local + hooks: + - id: pylint + name: pylint + entry: poetry run pylint + language: system + types: [python] + args: + - "--disable=fixme" + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: "v0.0.262" + hooks: + - id: ruff + - repo: local + hooks: + - id: mypy + name: mypy + entry: poetry run mypy + language: system + types: [python] + args: + - '--exclude=/\.venv\/' diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5f4545a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,39 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Server", + "type": "python", + "request": "launch", + "module": "server", + "justMyCode": false, + "env": { + "APP_DATA_DIR": "${workspaceFolder}/data", + "KEY": "CHANGEME", + "AUTH_TYPE": "basic", + "BASIC_AUTH_USER": "headscale", + "BASIC_AUTH_PASS": "headscale", + "OIDC_LOGOUT_REDIRECT_URI": "http://localhost:5000/overview", + } + }, + { + "name": "Python: Server with coverage", + "type": "python", + "request": "launch", + "module": "coverage", + "args": ["run", "--omit", ".venv/**", "-m", "server"], + "justMyCode": false, + "env": { + "APP_DATA_DIR": "${workspaceFolder}/data", + "KEY": "CHANGEME", + "AUTH_TYPE": "basic", + "BASIC_AUTH_USER": "headscale", + "BASIC_AUTH_PASS": "headscale", + "OIDC_LOGOUT_REDIRECT_URI": "http://localhost:5000/overview", + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a9adf05 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.analysis.extraPaths": [ + ".venv/src/flask-pydantic/flask_pydantic" + ], + "files.associations": { + "*.html": "jinja" + }, +} diff --git a/Dockerfile b/Dockerfile index c974d6a..37e408c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,14 +9,14 @@ ARG WORKDIR ENV PYTHONUNBUFFERED=1 # Don't create `.pyc` files: ENV PYTHONDONTWRITEBYTECODE=1 -# https://github.com/rust-lang/cargo/issues/2808 +# https://github.com/rust-lang/cargo/issues/2808 ENV CARGO_NET_GIT_FETCH_WITH_CLI=true # For building CFFI / Crypgotraphy (needed on ARM builds): RUN apk add gcc make musl-dev libffi-dev rust cargo git openssl-dev RUN pip install poetry -RUN poetry config virtualenvs.in-project true +RUN poetry config virtualenvs.in-project true WORKDIR ${WORKDIR} @@ -34,7 +34,7 @@ WORKDIR ${WORKDIR} RUN mkdir /app/instance && chown 1000:1000 /app/instance RUN mkdir /data -RUN chown 1000:1000 /data +RUN chown 1000:1000 /data RUN adduser app -DHh ${WORKDIR} -u 1000 USER 1000 diff --git a/Jenkinsfile b/Jenkinsfile index 6e3d935..f204c91 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,12 +3,11 @@ pipeline { label 'linux-x64' } environment { - APP_VERSION = 'v0.6.2' - HS_VERSION = "v0.22.1" // Version of Headscale this is compatible with + APP_VERSION = 'v0.7.0' + HS_VERSION = "v0.21.1" // Version of Headscale this is compatible with BUILD_DATE = '' BUILDER_NAME = "multiarch-${env.BUILD_TAG}" - DOCKERHUB_CRED = credentials('dockerhub-ifargle-pat') GHCR_URL = "https://ghcr.io/" @@ -62,7 +61,7 @@ pipeline { --label \"GIT_COMMIT=${env.GIT_COMMIT}\" \ --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 \ --push - """ + """ } else { // If I'm just testing, I don't need to build for ARM sh """ docker buildx build . \ @@ -98,10 +97,10 @@ pipeline { } else { sh """ - docker pull git.sysctl.io/albert/headscale-webui:testing - docker pull ghcr.io/ifargle/headscale-webui:testing - docker pull git.sysctl.io/albert/headscale-webui:${env.BRANCH_NAME} - docker pull ghcr.io/ifargle/headscale-webui:${env.BRANCH_NAME} + docker pull git.sysctl.io/albert/headscale-webui:testing + docker pull ghcr.io/ifargle/headscale-webui:testing + docker pull git.sysctl.io/albert/headscale-webui:${env.BRANCH_NAME} + docker pull ghcr.io/ifargle/headscale-webui:${env.BRANCH_NAME} """ } } diff --git a/README.md b/README.md index b7c8420..44de349 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +Please pull 0.6.1 for now -- 0.7.x isn't working. My apologies! I will remove this when we have it working. Thank you for your patience. +

diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..1fe4801 --- /dev/null +++ b/auth.py @@ -0,0 +1,242 @@ +"""Headscale WebUI authentication abstraction.""" + +import secrets +from functools import wraps +from typing import Awaitable, Callable, Literal, ParamSpec, TypeVar + +import requests +from flask import current_app +from flask.typing import ResponseReturnValue +from flask_basicauth import BasicAuth # type: ignore +from flask_oidc import OpenIDConnect # type: ignore +from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field + +from config import BasicAuthConfig, Config, OidcAuthConfig + + +class OidcSecretsModel(BaseModel): + """OIDC secrets model used by the flask_oidc module.""" + + class OidcWebModel(BaseModel): + """OIDC secrets web model.""" + + issuer: AnyHttpUrl + auth_uri: AnyHttpUrl + client_id: str + client_secret: str = Field(hidden=True) + redirect_uris: list[AnyUrl] + userinfo_uri: AnyHttpUrl | None + token_uri: AnyHttpUrl + + web: OidcWebModel + + +class OpenIdProviderMetadata(BaseModel): + """OIDC Provider Metadata model. + + From https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + + TODO: Add default factories for some fields and maybe descriptions. + """ + + class Config: + """BaseModel configuration.""" + + extra = "allow" + """Used for logout_redirect_uri.""" + + issuer: AnyHttpUrl + authorization_endpoint: AnyHttpUrl + token_endpoint: AnyHttpUrl + userinfo_endpoint: AnyHttpUrl | None + jwks_uri: AnyHttpUrl + registration_endpoint: AnyHttpUrl | None + scopes_supported: list[str] + response_types_supported: list[ + Literal[ + "code", + "id_token", + "id_token token", + "code id_token", + "code token", + "code id_token token", + "none", + ] + ] + response_modes_supported: list[Literal["query", "fragment"]] | None + grant_types_supported: list[str] | None + acr_values_supported: list[str] | None + subject_types_supported: list[str] + id_token_signing_alg_values_supported: list[str] + id_token_encryption_alg_values_supported: list[str] | None + id_token_encryption_enc_values_supported: list[str] | None + userinfo_signing_alg_values_supported: list[str | None] | None + userinfo_encryption_alg_values_supported: list[str] | None + userinfo_encryption_enc_values_supported: list[str] | None + request_object_signing_alg_values_supported: list[str] | None + request_object_encryption_alg_values_supported: list[str] | None + request_object_encryption_enc_values_supported: list[str] | None + token_endpoint_auth_methods_supported: list[str] | None + token_endpoint_auth_signing_alg_values_supported: list[str] | None + display_values_supported: list[Literal["page", "popup", "touch", "wap"]] | None + claim_types_supported: list[Literal["normal", "aggregated", "distributed"]] | None + claims_supported: list[str] | None + service_documentation: AnyUrl | None + claims_locales_supported: list[str] | None + ui_locales_supported: list[str] | None + claims_parameter_supported: bool = Field(False) + request_parameter_supported: bool = Field(False) + request_uri_parameter_supported: bool = Field(True) + require_request_uri_registration: bool = Field(False) + op_policy_uri: AnyUrl | None + op_tos_uri: AnyUrl | None + + +T = TypeVar("T") +P = ParamSpec("P") + + +class AuthManager: + """Authentication manager.""" + + def __init__(self, config: Config, request_timeout: float = 10) -> None: + """Initialize the authentication manager. + + Arguments: + config -- main application configuration. + + Keyword Arguments: + request_timeout -- timeout for OIDC request (default: {10}) + """ + self._gui_url = config.domain_name + config.base_path + self._auth_type = config.auth_type + self._auth_config = config.auth_type.config + self._logout_url: str | None = None + self._request_timeout = request_timeout + + match self._auth_config: + case BasicAuthConfig(): + current_app.logger.info( + "Loading basic auth libraries and configuring app..." + ) + + current_app.config["BASIC_AUTH_USERNAME"] = self._auth_config.username + current_app.config["BASIC_AUTH_PASSWORD"] = self._auth_config.password + current_app.config["BASIC_AUTH_FORCE"] = True + + # TODO: Change for flask-httpauth – flask_basicauth is not maintained. + self._auth_handler = BasicAuth(current_app) + case OidcAuthConfig(): + current_app.logger.info("Loading OIDC libraries and configuring app...") + + oidc_info = OpenIdProviderMetadata.parse_obj( + requests.get( + self._auth_config.auth_url, timeout=request_timeout + ).json() + ) + current_app.logger.debug( + "JSON dump for OIDC_INFO: %s", oidc_info.json() + ) + + client_secrets = OidcSecretsModel( + web=OidcSecretsModel.OidcWebModel( + issuer=oidc_info.issuer, + auth_uri=oidc_info.authorization_endpoint, + client_id=self._auth_config.client_id, + client_secret=self._auth_config.secret, + redirect_uris=[ + AnyUrl( + f"{config.domain_name}{config.base_path}/oidc_callback", + scheme="", + ) + ], + userinfo_uri=oidc_info.userinfo_endpoint, + token_uri=oidc_info.token_endpoint, + ) + ) + + # Make the best effort to create the data directory. + try: + config.app_data_dir.mkdir(parents=True, exist_ok=True) + except PermissionError: + current_app.logger.warning( + "Tried and failed to create data directory %s.", + config.app_data_dir, + ) + + oidc_secrets_path = config.app_data_dir / "secrets.json" + with open(oidc_secrets_path, "w+", encoding="utf-8") as secrets_file: + secrets_file.write(client_secrets.json()) + + current_app.config.update( # type: ignore + { + "SECRET_KEY": secrets.token_urlsafe(32), + "TESTING": config.debug_mode, + "DEBUG": config.debug_mode, + "OIDC_CLIENT_SECRETS": oidc_secrets_path, + "OIDC_ID_TOKEN_COOKIE_SECURE": True, + "OIDC_REQUIRE_VERIFIED_EMAIL": False, + "OIDC_USER_INFO_ENABLED": True, + "OIDC_OPENID_REALM": "Headscale-WebUI", + "OIDC_SCOPES": ["openid", "profile", "email"], + "OIDC_INTROSPECTION_AUTH_METHOD": "client_secret_post", + } + ) + + self._logout_url = getattr(oidc_info, "end_session_endpoint", None) + + self._auth_handler = OpenIDConnect(current_app) + + def require_login( + self, + func: Callable[P, ResponseReturnValue] + | Callable[P, Awaitable[ResponseReturnValue]], + ) -> Callable[P, ResponseReturnValue]: + """Guard decorator used for restricting access to the Flask page. + + Uses OIDC or Basic auth depending on configuration. + """ + + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> ResponseReturnValue: + sync_func = current_app.ensure_sync(func) # type: ignore + sync_func.__name__ = f"{func.__name__}" + + # OIDC + # TODO: Add user group restrictions. + if isinstance(self._auth_handler, OpenIDConnect): + return self._auth_handler.require_login(sync_func)( # type: ignore + *args, **kwargs + ) + + # Basic auth + return self._auth_handler.required(sync_func)( # type: ignore + *args, **kwargs + ) + + return wrapper + + def logout(self) -> str | None: + """Execute logout with the auth provider.""" + # Logout is only applicable for OIDC. + if isinstance(self._auth_handler, OpenIDConnect): + self._auth_handler.logout() + + if isinstance(self._auth_config, OidcAuthConfig): + if self._logout_url is not None: + logout_url = self._logout_url + if self._auth_config.logout_redirect_uri is not None: + logout_url += ( + "?post_logout_redirect_uri=" + + self._auth_config.logout_redirect_uri + ) + return logout_url + + return None + + @property + def oidc_handler(self) -> OpenIDConnect | None: + """Get the OIDC handler if exists.""" + if isinstance(self._auth_handler, OpenIDConnect): + return self._auth_handler + return None diff --git a/config.py b/config.py new file mode 100644 index 0000000..886c12f --- /dev/null +++ b/config.py @@ -0,0 +1,545 @@ +"""Headscale WebUI configuration.""" + +import importlib.metadata +import itertools +import os +from dataclasses import dataclass +from datetime import datetime +from enum import StrEnum +from logging import getLevelNamesMapping +from pathlib import Path +from typing import Any, Type +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + +from aiohttp import ClientConnectionError +from flask import current_app +from pydantic import validator # type: ignore +from pydantic import ( + AnyUrl, + BaseModel, + BaseSettings, + ConstrainedStr, + Field, + ValidationError, +) + +import helper + + +class OidcAuthConfig(BaseSettings): + """OpenID Connect authentication configuration. + + Used only if "AUTH_TYPE" environment variable is set to "oidc". + """ + + auth_url: str = Field( + ..., + env="OIDC_AUTH_URL", + description=( + "URL to OIDC auth endpoint. Example: " + '"https://example.com/.well-known/openid-configuration"' + ), + ) + client_id: str = Field( + env="OIDC_CLIENT_ID", + description="OIDC client ID.", + ) + secret: str = Field( + env="OIDC_CLIENT_SECRET", + description="OIDC client secret.", + ) + logout_redirect_uri: str | None = Field( + None, + env="OIDC_LOGOUT_REDIRECT_URI", + description="Optional OIDC redirect URL to follow after logout.", + ) + + +class BasicAuthConfig(BaseSettings): + """Basic auth authentication configuration. + + Used only if "AUTH_TYPE" environment variable is set to "basic". + """ + + username: str = Field( + "headscale", env="BASIC_AUTH_USER", description="Username for basic auth." + ) + password: str = Field( + "headscale", env="BASIC_AUTH_PASS", description="Password for basic auth." + ) + + +class AuthType(StrEnum): + """Authentication type.""" + + BASIC = "basic" + OIDC = "oidc" + + @property + def config(self): + """Get configuration depending on enum value.""" + match self: + case self.BASIC: + return BasicAuthConfig() # type: ignore + case self.OIDC: + return OidcAuthConfig() # type: ignore + + +class _LowerConstr(ConstrainedStr): + """String with lowercase transformation.""" + + to_lower = True + + +@dataclass +class InitCheckErrorModel: + """Initialization check error model.""" + + title: str + details: str + + def print_to_logger(self): + """Print the error information to logger.""" + current_app.logger.critical(self.title) + + def format_message(self) -> str: + """Format message for the error page.""" + return helper.format_message( + helper.MessageErrorType.ERROR, self.title, f"

{self.details}

" + ) + + +@dataclass +class InitCheckError(RuntimeError): + """Initialization check error.""" + + errors: list[InitCheckErrorModel] | InitCheckErrorModel | None = None + + def append_error(self, error: InitCheckErrorModel): + """Append error to the errors collection.""" + match self.errors: + case InitCheckErrorModel(): + self.errors = [self.errors, error] + case list(): + self.errors.append(error) + case _: + self.errors = error + + def __iter__(self): # noqa + match self.errors: + case InitCheckErrorModel(): + yield self.errors + case list(): + for error in self.errors: + yield error + case _: + return + + @classmethod + def from_validation_error(cls, error: ValidationError): + """Create an InitCheckError from Pydantic's ValidationError.""" + current_app.logger.critical( + "Following environment variables are required but are not declared or have " + "an invalid value:" + ) + + new_error = cls() + for sub_pydantic_error in error.errors(): + pydantic_name = sub_pydantic_error["loc"][0] + assert isinstance( + pydantic_name, str + ), "Configuration class malformed. Raise issue on GitHub." + + model: Type[BaseModel] = error.model # type: ignore + field = model.__fields__[pydantic_name] + assert ( + "env" in field.field_info.extra + ), "Environment variable name not set. Raise issue on GitHub." + + current_app.logger.critical( + " %s with type %s: %s", + field.field_info.extra["env"], + field.type_.__name__, + sub_pydantic_error["msg"], + ) + + new_error.append_error( + InitCheckErrorModel( + f"Environment error for {field.field_info.extra['env']}", + f"Required variable {field.field_info.extra['env']} with type " + f'"{field.type_.__name__}" validation error ' + f"({sub_pydantic_error['type']}): {sub_pydantic_error['msg']}. " + f"Variable description: {field.field_info.description}", + ) + ) + return new_error + + @classmethod + def from_client_connection_error(cls, error: ClientConnectionError): + """Create an InitCheckError from aiohttp's ClientConnectionError.""" + return InitCheckError( + InitCheckErrorModel( + "Headscale server API is unreachable.", + "Your headscale server is either unreachable or not properly " + "configured. Please ensure your configuration is correct. Error" + f"details: {error}", + ) + ) + + @classmethod + def from_exception(cls, error: Exception, print_to_logger: bool = True): + """Create an InitCheckError from any error. + + Some special cases are handled separately. + """ + if isinstance(error, InitCheckError): + new_error = error + elif isinstance(error, ValidationError): + new_error = cls.from_validation_error(error) + elif isinstance(error, ClientConnectionError): + new_error = cls.from_client_connection_error(error) + else: + new_error = cls( + InitCheckErrorModel( + f"Unexpected error occurred: {error.__class__.__name__}. Raise an " + "issue on GitHub.", + str(error), + ) + ) + if print_to_logger: + for sub_error in new_error: + sub_error.print_to_logger() + + return new_error + + +def _get_version_from_package(): + """Get package version from metadata if not given from environment.""" + return importlib.metadata.version("headscale-webui") + + +def _get_default_build_date(): + """Get a default build date is none is provided.""" + return str(datetime.now()) + + +# Functions to get git-related information in development scenario, where no relevant +# environment variables are set. If not in git repository fall back to unknown values. +# GitPython is added as dev dependency, thus we need to have fallback in case of +# production environment. +try: + from git.exc import GitError + from git.repo import Repo + + def _get_default_git_branch() -> str: + try: + return Repo(search_parent_directories=True).head.ref.name + except GitError as error: + return f"Error getting branch name: {error}" + + def _get_default_git_commit() -> str: + try: + return Repo(search_parent_directories=True).head.ref.object.hexsha + except GitError as error: + return f"Error getting commit ID: {error}" + + def _get_default_git_repo_url_gitpython() -> str | None: + try: + return ( + Repo(search_parent_directories=True) + .remotes[0] + .url.replace("git@github.com:", "https://github.com/") + .removesuffix(".git") + ) + except (GitError, IndexError): + return None + +except ImportError: + + def _get_default_git_branch() -> str: + return "UNKNOWN" + + def _get_default_git_commit() -> str: + return "UNKNOWN" + + def _get_default_git_repo_url_gitpython() -> str | None: + return None + + +def _get_default_git_repo_url(): + gitpython = _get_default_git_repo_url_gitpython() + return ( + "https://github.com/iFargle/headscale-webui" if gitpython is None else gitpython + ) + + +class Config(BaseSettings): + """Headscale WebUI configuration. + + `env` arg means what is the environment variable called. + """ + + color: _LowerConstr = Field( + "red", + env="COLOR", + description=( + "Preferred color scheme. See the MaterializeCSS docs " + "(https://materializecss.github.io/materialize/color.html#palette) for " + 'examples. Only set the "base" color, e.g., instead of `blue-gray ' + "darken-1` use `blue-gray`." + ), + ) + auth_type: AuthType = Field( + AuthType.BASIC, + env="AUTH_TYPE", + description="Authentication type.", + ) + log_level_name: str = Field( + "INFO", + env="LOG_LEVEL", + description=( + 'Logger level. If "DEBUG", Flask debug mode is activated, so don\'t use it ' + "in production." + ), + ) + debug_mode: bool = Field( + False, + env="DEBUG_MODE", + description="Enable Flask debug mode.", + ) + # TODO: Use user's locale to present datetime, not from server-side constant. + timezone: ZoneInfo = Field( + "UTC", + env="TZ", + description='Default time zone in IANA format. Example: "Asia/Tokyo".', + ) + key: str = Field( + env="KEY", + description=( + "Encryption key. Set this to a random value generated from " + "`openssl rand -base64 32`." + ), + ) + + app_version: str = Field( + default_factory=_get_version_from_package, + env="APP_VERSION", + description="Application version. Should be set by Docker.", + ) + build_date: str = Field( + default_factory=_get_default_build_date, + env="BUILD_DATE", + description="Application build date. Should be set by Docker.", + ) + git_branch: str = Field( + default_factory=_get_default_git_branch, + env="GIT_BRANCH", + description="Application git branch. Should be set by Docker.", + ) + git_commit: str = Field( + default_factory=_get_default_git_commit, + env="GIT_COMMIT", + description="Application git commit. Should be set by Docker.", + ) + git_repo_url: AnyUrl = Field( + default_factory=_get_default_git_repo_url, + env="GIT_REPO_URL", + description=( + "Application git repository URL. " + "Set automatically either to local or default repository." + ), + ) + + # TODO: Autogenerate in headscale_api. + hs_version: str = Field( + "UNKNOWN", + env="HS_VERSION", + description=( + "Version of Headscale this is compatible with. Should be set by Docker." + ), + ) + hs_server: AnyUrl = Field( + "http://localhost:5000", + env="HS_SERVER", + description="The URL of your Headscale control server.", + ) + hs_config_path: Path = Field( + None, + env="HS_CONFIG_PATH", + description=( + "Path to the Headscale configuration. Default paths are tried if not set." + ), + ) + + domain_name: AnyUrl = Field( + "http://localhost:5000", + env="DOMAIN_NAME", + description="Base domain name of the Headscale WebUI.", + ) + base_path: str = Field( + "", + env="SCRIPT_NAME", + description=( + 'The "Base Path" for hosting. For example, if you want to host on ' + "http://example.com/admin, set this to `/admin`, otherwise remove this " + "variable entirely." + ), + ) + + app_data_dir: Path = Field( + Path("/data"), + env="APP_DATA_DIR", + description="Application data path.", + ) + + @validator("auth_type", pre=True) + @classmethod + def validate_auth_type(cls, value: Any): + """Validate AUTH_TYPE so that it accepts more valid values.""" + value = str(value).lower() + if value == "": + return AuthType.BASIC + return AuthType(value) + + @validator("log_level_name") + @classmethod + def validate_log_level_name(cls, value: Any): + """Validate log_level_name field. + + Check if matches allowed log level from logging Python module. + """ + assert isinstance(value, str) + value = value.upper() + allowed_levels = getLevelNamesMapping() + if value not in allowed_levels: + raise ValueError( + f'Unkown log level "{value}". Select from: ' + + ", ".join(allowed_levels.keys()) + ) + return value + + @validator("timezone", pre=True) + @classmethod + def validate_timezone(cls, value: Any): + """Validate and parse timezone information.""" + try: + return ZoneInfo(value) + except ZoneInfoNotFoundError as error: + raise ValueError(f"Timezone {value} is invalid: {error}") from error + + @validator("hs_config_path", pre=True) + @classmethod + def validate_hs_config_path(cls, value: Any): + """Validate Headscale configuration path. + + If none is given, some default paths that Headscale itself is using for lookup + are searched. + """ + if value is None: + search_base = ["/etc/headscale", Path.home() / ".headscale"] + suffixes = ["yml", "yaml", "json"] + else: + assert isinstance(value, (str, Path)) + search_base = [value] + suffixes = [""] + + for base, suffix in itertools.product(search_base, suffixes): + cur_path = f"{base}/config.{suffix}" + if os.access(cur_path, os.R_OK): + return cur_path + + raise InitCheckError( + InitCheckErrorModel( + "Headscale configuration read failed.", + "Please ensure your headscale configuration file resides in " + '/etc/headscale or in ~/.headscale and is named "config.yaml", ' + '"config.yml" or "config.json".', + ) + ) + + @validator("base_path") + @classmethod + def validate_base_path(cls, value: Any): + """Validate base path.""" + assert isinstance(value, str) + if value == "/": + return "" + return value + + @validator("app_data_dir") + @classmethod + def validate_app_data_dir(cls, value: Path): + """Validate application data format and basic filesystem access.""" + err = InitCheckError() + + if not os.access(value, os.R_OK): + err.append_error( + InitCheckErrorModel( + f"Data ({value}) folder not readable.", + f'"{value}" is not readable. Please ensure your permissions are ' + "correct. Data should be readable by UID/GID 1000:1000.", + ) + ) + + if not os.access(value, os.W_OK): + err.append_error( + InitCheckErrorModel( + f"Data ({value}) folder not writable.", + f'"{value}" is not writable. Please ensure your permissions are ' + "correct. Data should be writable by UID/GID 1000:1000.", + ) + ) + + if not os.access(value, os.X_OK): + err.append_error( + InitCheckErrorModel( + f"Data ({value}) folder not executable.", + f'"{value}" is not executable. Please ensure your permissions are ' + "correct. Data should be executable by UID/GID 1000:1000.", + ) + ) + + key_file = value / "key.txt" + if key_file.exists(): + if not os.access(key_file, os.R_OK): + err.append_error( + InitCheckErrorModel( + f"Key file ({key_file}) not readable.", + f'"{key_file}" is not readable. Please ensure your permissions ' + "are correct. It should be readable by UID/GID 1000:1000.", + ) + ) + + if not os.access(key_file, os.W_OK): + err.append_error( + InitCheckErrorModel( + f"Key file ({key_file}) not writable.", + f'"{key_file}" is not writable. Please ensure your permissions ' + "are correct. It should be writable by UID/GID 1000:1000.", + ) + ) + + if err.errors is not None: + raise err + + return value + + @property + def log_level(self) -> int: + """Get integer log level.""" + return getLevelNamesMapping()[self.log_level_name] + + @property + def color_nav(self): + """Get navigation color.""" + return f"{self.color} darken-1" + + @property + def color_btn(self): + """Get button color.""" + return f"{self.color} darken-3" + + @property + def key_file(self): + """Get key file path.""" + return self.app_data_dir / "key.txt" diff --git a/headscale.py b/headscale.py index df678cd..50dd6c7 100644 --- a/headscale.py +++ b/headscale.py @@ -1,437 +1,129 @@ -# pylint: disable=wrong-import-order +"""Headscale API abstraction.""" + +from functools import wraps +from typing import Awaitable, Callable, ParamSpec, TypeVar -import requests, json, os, logging, yaml from cryptography.fernet import Fernet -from datetime import timedelta, date -from dateutil import parser -from flask import Flask -from dotenv import load_dotenv +from flask import current_app, redirect, url_for +from flask.typing import ResponseReturnValue +from headscale_api.config import HeadscaleConfig as HeadscaleConfigBase +from headscale_api.headscale import Headscale, UnauthorizedError +from pydantic import ValidationError -load_dotenv() -LOG_LEVEL = os.environ["LOG_LEVEL"].replace('"', '').upper() -DATA_DIRECTORY = os.environ["DATA_DIRECTORY"].replace('"', '') if os.environ["DATA_DIRECTORY"] else "/data" -# Initiate the Flask application and logging: -app = Flask(__name__, static_url_path="/static") -match LOG_LEVEL: - case "DEBUG" : app.logger.setLevel(logging.DEBUG) - case "INFO" : app.logger.setLevel(logging.INFO) - case "WARNING" : app.logger.setLevel(logging.WARNING) - case "ERROR" : app.logger.setLevel(logging.ERROR) - case "CRITICAL": app.logger.setLevel(logging.CRITICAL) +from config import Config -################################################################## -# Functions related to HEADSCALE and API KEYS -################################################################## -def get_url(inpage=False): - if not inpage: - return os.environ['HS_SERVER'] - config_file = "" - try: - config_file = open("/etc/headscale/config.yml", "r") - app.logger.info("Opening /etc/headscale/config.yml") - except: - config_file = open("/etc/headscale/config.yaml", "r") - app.logger.info("Opening /etc/headscale/config.yaml") - config_yaml = yaml.safe_load(config_file) - if "server_url" in config_yaml: - return str(config_yaml["server_url"]) - app.logger.warning("Failed to find server_url in the config. Falling back to ENV variable") - return os.environ['HS_SERVER'] +T = TypeVar("T") +P = ParamSpec("P") -def set_api_key(api_key): - # User-set encryption key - encryption_key = os.environ['KEY'] - # Key file on the filesystem for persistent storage - key_file = open(os.path.join(DATA_DIRECTORY, "key.txt"), "wb+") - # Preparing the Fernet class with the key - fernet = Fernet(encryption_key) - # Encrypting the key - encrypted_key = fernet.encrypt(api_key.encode()) - # Return true if the file wrote correctly - return True if key_file.write(encrypted_key) else False -def get_api_key(): - if not os.path.exists(os.path.join(DATA_DIRECTORY, "key.txt")): return False - # User-set encryption key - encryption_key = os.environ['KEY'] - # Key file on the filesystem for persistent storage - key_file = open(os.path.join(DATA_DIRECTORY, "key.txt"), "rb+") - # The encrypted key read from the file - enc_api_key = key_file.read() - if enc_api_key == b'': return "NULL" +class HeadscaleApi(Headscale): + """Headscale API abstraction.""" - # Preparing the Fernet class with the key - fernet = Fernet(encryption_key) - # Decrypting the key - decrypted_key = fernet.decrypt(enc_api_key).decode() + def __init__(self, config: Config, requests_timeout: float = 10): + """Initialize the Headscale API abstraction. - return decrypted_key + Arguments: + config -- Headscale WebUI configuration. -def test_api_key(url, api_key): - response = requests.get( - str(url)+"/api/v1/apikey", - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - return response.status_code - -# Expires an API key -def expire_key(url, api_key): - payload = {'prefix':str(api_key[0:10])} - json_payload=json.dumps(payload) - app.logger.debug("Sending the payload '"+str(json_payload)+"' to the headscale server") - - response = requests.post( - str(url)+"/api/v1/apikey/expire", - data=json_payload, - headers={ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - return response.status_code - -# Checks if the key needs to be renewed -# If it does, renews the key, then expires the old key -def renew_api_key(url, api_key): - # 0 = Key has been updated or key is not in need of an update - # 1 = Key has failed validity check or has failed to write the API key - # Check when the key expires and compare it to todays date: - key_info = get_api_key_info(url, api_key) - expiration_time = key_info["expiration"] - today_date = date.today() - expire = parser.parse(expiration_time) - expire_fmt = str(expire.year) + "-" + str(expire.month).zfill(2) + "-" + str(expire.day).zfill(2) - expire_date = date.fromisoformat(expire_fmt) - delta = expire_date - today_date - tmp = today_date + timedelta(days=90) - new_expiration_date = str(tmp)+"T00:00:00.000000Z" - - # If the delta is less than 5 days, renew the key: - if delta < timedelta(days=5): - app.logger.warning("Key is about to expire. Delta is "+str(delta)) - payload = {'expiration':str(new_expiration_date)} - json_payload=json.dumps(payload) - app.logger.debug("Sending the payload '"+str(json_payload)+"' to the headscale server") - - response = requests.post( - str(url)+"/api/v1/apikey", - data=json_payload, - headers={ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } + Keyword Arguments: + requests_timeout -- timeout of API requests in seconds (default: {10}) + """ + self._config = config + self._hs_config: HeadscaleConfigBase | None = None + self._api_key: str | None = None + self.logger = current_app.logger + super().__init__( + self.base_url, + self.api_key, + requests_timeout, + raise_exception_on_error=False, + logger=current_app.logger, ) - new_key = response.json() - app.logger.debug("JSON: "+json.dumps(new_key)) - app.logger.debug("New Key is: "+new_key["apiKey"]) - api_key_test = test_api_key(url, new_key["apiKey"]) - app.logger.debug("Testing the key: "+str(api_key_test)) - # Test if the new key works: - if api_key_test == 200: - app.logger.info("The new key is valid and we are writing it to the file") - if not set_api_key(new_key["apiKey"]): - app.logger.error("We failed writing the new key!") - return False # Key write failed - app.logger.info("Key validated and written. Moving to expire the key.") - expire_key(url, api_key) - return True # Key updated and validated - else: - app.logger.error("Testing the API key failed.") - return False # The API Key test failed - else: return True # No work is required -# Gets information about the current API key -def get_api_key_info(url, api_key): - app.logger.info("Getting API key information") - response = requests.get( - str(url)+"/api/v1/apikey", - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - json_response = response.json() - # Find the current key in the array: - key_prefix = str(api_key[0:10]) - app.logger.info("Looking for valid API Key...") - for key in json_response["apiKeys"]: - if key_prefix == key["prefix"]: - app.logger.info("Key found.") - return key - app.logger.error("Could not find a valid key in Headscale. Need a new API key.") - return "Key not found" + @property + def app_config(self) -> Config: + """Get Headscale WebUI configuration.""" + return self._config -################################################################## -# Functions related to MACHINES -################################################################## + @property + def hs_config(self) -> HeadscaleConfigBase | None: + """Get Headscale configuration and cache on success. -# register a new machine -def register_machine(url, api_key, machine_key, user): - app.logger.info("Registering machine %s to user %s", str(machine_key), str(user)) - response = requests.post( - str(url)+"/api/v1/machine/register?user="+str(user)+"&key="+str(machine_key), - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - return response.json() + Returns: + Headscale configuration if a valid configuration has been found. + """ + if self._hs_config is not None: + return self._hs_config + try: + return HeadscaleConfigBase.parse_file(self._config.hs_config_path) + except ValidationError as error: + self.logger.warning( + "Following errors happened when tried to parse Headscale config:" + ) + for sub_error in str(error).splitlines(): + self.logger.warning(" %s", sub_error) + return None -# Sets the machines tags -def set_machine_tags(url, api_key, machine_id, tags_list): - app.logger.info("Setting machine_id %s tag %s", str(machine_id), str(tags_list)) - response = requests.post( - str(url)+"/api/v1/machine/"+str(machine_id)+"/tags", - data=tags_list, - headers={ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - return response.json() + @property + def base_url(self) -> str: + """Get base URL of the Headscale server. -# Moves machine_id to user "new_user" -def move_user(url, api_key, machine_id, new_user): - app.logger.info("Moving machine_id %s to user %s", str(machine_id), str(new_user)) - response = requests.post( - str(url)+"/api/v1/machine/"+str(machine_id)+"/user?user="+str(new_user), - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - return response.json() + Tries to load it from Headscale config, otherwise falls back to WebUI config. + """ + if self.hs_config is None or self.hs_config.server_url is None: + self.logger.warning( + 'Failed to find "server_url" in the Headscale config. Falling back to ' + "the environment variable." + ) + return self._config.hs_server -def update_route(url, api_key, route_id, current_state): - action = "disable" if current_state == "True" else "enable" + return self.hs_config.server_url - app.logger.info("Updating Route %s: Action: %s", str(route_id), str(action)) + @property + def api_key(self) -> str | None: + """Get API key from cache or from file.""" + if self._api_key is not None: + return self._api_key - # Debug - app.logger.debug("URL: "+str(url)) - app.logger.debug("Route ID: "+str(route_id)) - app.logger.debug("Current State: "+str(current_state)) - app.logger.debug("Action to take: "+str(action)) + if not self._config.key_file.exists(): + return None - response = requests.post( - str(url)+"/api/v1/routes/"+str(route_id)+"/"+str(action), - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - return response.json() + with open(self._config.key_file, "rb") as key_file: + enc_api_key = key_file.read() + if enc_api_key == b"": + return None -# Get all machines on the Headscale network -def get_machines(url, api_key): - app.logger.info("Getting machine information") - response = requests.get( - str(url)+"/api/v1/machine", - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - return response.json() + self._api_key = Fernet(self._config.key).decrypt(enc_api_key).decode() + return self._api_key -# Get machine with "machine_id" on the Headscale network -def get_machine_info(url, api_key, machine_id): - app.logger.info("Getting information for machine ID %s", str(machine_id)) - response = requests.get( - str(url)+"/api/v1/machine/"+str(machine_id), - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - return response.json() + @api_key.setter + def api_key(self, new_api_key: str): + """Write the new API key to file and store in cache.""" + with open(self._config.key_file, "wb") as key_file: + key_file.write(Fernet(self._config.key).encrypt(new_api_key.encode())) -# Delete a machine from Headscale -def delete_machine(url, api_key, machine_id): - app.logger.info("Deleting machine %s", str(machine_id)) - response = requests.delete( - str(url)+"/api/v1/machine/"+str(machine_id), - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - status = "True" if response.status_code == 200 else "False" - if response.status_code == 200: - app.logger.info("Machine deleted.") - else: - app.logger.error("Deleting machine failed! %s", str(response.json())) - return {"status": status, "body": response.json()} + # Save to local cache only after successful file write. + self._api_key = new_api_key -# Rename "machine_id" with name "new_name" -def rename_machine(url, api_key, machine_id, new_name): - app.logger.info("Renaming machine %s", str(machine_id)) - response = requests.post( - str(url)+"/api/v1/machine/"+str(machine_id)+"/rename/"+str(new_name), - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - status = "True" if response.status_code == 200 else "False" - if response.status_code == 200: - app.logger.info("Machine renamed") - else: - app.logger.error("Machine rename failed! %s", str(response.json())) - return {"status": status, "body": response.json()} + def key_check_guard( + self, func: Callable[P, T] | Callable[P, Awaitable[T]] + ) -> Callable[P, T | ResponseReturnValue]: + """Ensure the validity of a Headscale API key with decorator. -# Gets routes for the passed machine_id -def get_machine_routes(url, api_key, machine_id): - app.logger.info("Getting routes for machine %s", str(machine_id)) - response = requests.get( - str(url)+"/api/v1/machine/"+str(machine_id)+"/routes", - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - if response.status_code == 200: - app.logger.info("Routes obtained") - else: - app.logger.error("Failed to get routes: %s", str(response.json())) - return response.json() + Also, it checks if the key needs renewal and if it is invalid redirects to the + settings page. + """ -# Gets routes for the entire tailnet -def get_routes(url, api_key): - app.logger.info("Getting routes") - response = requests.get( - str(url)+"/api/v1/routes", - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - return response.json() -################################################################## -# Functions related to USERS -################################################################## + @wraps(func) + def decorated(*args: P.args, **kwargs: P.kwargs) -> T | ResponseReturnValue: + try: + return current_app.ensure_sync(func)(*args, **kwargs) # type: ignore + except UnauthorizedError: + current_app.logger.warning( + "Detected unauthorized error from Headscale API. " + "Redirecting to settings." + ) + return redirect(url_for("settings_page")) -# Get all users in use -def get_users(url, api_key): - app.logger.info("Getting Users") - response = requests.get( - str(url)+"/api/v1/user", - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - return response.json() - -# Rename "old_name" with name "new_name" -def rename_user(url, api_key, old_name, new_name): - app.logger.info("Renaming user %s to %s.", str(old_name), str(new_name)) - response = requests.post( - str(url)+"/api/v1/user/"+str(old_name)+"/rename/"+str(new_name), - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - status = "True" if response.status_code == 200 else "False" - if response.status_code == 200: - app.logger.info("User renamed.") - else: - app.logger.error("Renaming User failed!") - return {"status": status, "body": response.json()} - -# Delete a user from Headscale -def delete_user(url, api_key, user_name): - app.logger.info("Deleting a User: %s", str(user_name)) - response = requests.delete( - str(url)+"/api/v1/user/"+str(user_name), - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - status = "True" if response.status_code == 200 else "False" - if response.status_code == 200: - app.logger.info("User deleted.") - else: - app.logger.error("Deleting User failed!") - return {"status": status, "body": response.json()} - -# Add a user from Headscale -def add_user(url, api_key, data): - app.logger.info("Adding user: %s", str(data)) - response = requests.post( - str(url)+"/api/v1/user", - data=data, - headers={ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - status = "True" if response.status_code == 200 else "False" - if response.status_code == 200: - app.logger.info("User added.") - else: - app.logger.error("Adding User failed!") - return {"status": status, "body": response.json()} - -################################################################## -# Functions related to PREAUTH KEYS in USERS -################################################################## - -# Get all PreAuth keys associated with a user "user_name" -def get_preauth_keys(url, api_key, user_name): - app.logger.info("Getting PreAuth Keys in User %s", str(user_name)) - response = requests.get( - str(url)+"/api/v1/preauthkey?user="+str(user_name), - headers={ - 'Accept': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - return response.json() - -# Add a preauth key to the user "user_name" given the booleans "ephemeral" -# and "reusable" with the expiration date "date" contained in the JSON payload "data" -def add_preauth_key(url, api_key, data): - app.logger.info("Adding PreAuth Key: %s", str(data)) - response = requests.post( - str(url)+"/api/v1/preauthkey", - data=data, - headers={ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - status = "True" if response.status_code == 200 else "False" - if response.status_code == 200: - app.logger.info("PreAuth Key added.") - else: - app.logger.error("Adding PreAuth Key failed!") - return {"status": status, "body": response.json()} - -# Expire a pre-auth key. data is {"user": "string", "key": "string"} -def expire_preauth_key(url, api_key, data): - app.logger.info("Expiring PreAuth Key...") - response = requests.post( - str(url)+"/api/v1/preauthkey/expire", - data=data, - headers={ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer '+str(api_key) - } - ) - status = "True" if response.status_code == 200 else "False" - app.logger.debug("expire_preauth_key - Return: "+str(response.json())) - app.logger.debug("expire_preauth_key - Status: "+str(status)) - return {"status": status, "body": response.json()} + return decorated diff --git a/helper.py b/helper.py index 82a4c3c..b048997 100644 --- a/helper.py +++ b/helper.py @@ -1,302 +1,152 @@ -# pylint: disable=wrong-import-order +"""Helper functions used for formatting.""" -import os, headscale, requests, logging -from flask import Flask +from datetime import timedelta +from enum import StrEnum +from typing import Literal -LOG_LEVEL = os.environ["LOG_LEVEL"].replace('"', '').upper() -DATA_DIRECTORY = os.environ["DATA_DIRECTORY"].replace('"', '') if os.environ["DATA_DIRECTORY"] else "/data" -# Initiate the Flask application and logging: -app = Flask(__name__, static_url_path="/static") -match LOG_LEVEL: - case "DEBUG" : app.logger.setLevel(logging.DEBUG) - case "INFO" : app.logger.setLevel(logging.INFO) - case "WARNING" : app.logger.setLevel(logging.WARNING) - case "ERROR" : app.logger.setLevel(logging.ERROR) - case "CRITICAL": app.logger.setLevel(logging.CRITICAL) -def pretty_print_duration(duration, delta_type=""): - """ Prints a duration in human-readable formats """ +def pretty_print_duration( + duration: timedelta, delta_type: Literal["expiry", ""] = "" +): # pylint: disable=too-many-return-statements + """Print a duration in human-readable format.""" days, seconds = duration.days, duration.seconds - hours = (days * 24 + seconds // 3600) - mins = (seconds % 3600) // 60 - secs = seconds % 60 + hours = days * 24 + seconds // 3600 + mins = (seconds % 3600) // 60 + secs = seconds % 60 if delta_type == "expiry": - if days > 730: return "in greater than two years" - if days > 365: return "in greater than a year" - if days > 0 : return "in "+ str(days ) + " days" if days > 1 else "in "+ str(days ) + " day" - if hours > 0 : return "in "+ str(hours) + " hours" if hours > 1 else "in "+ str(hours) + " hour" - if mins > 0 : return "in "+ str(mins ) + " minutes" if mins > 1 else "in "+ str(mins ) + " minute" - return "in "+ str(secs ) + " seconds" if secs >= 1 or secs == 0 else "in "+ str(secs ) + " second" - if days > 730: return "over two years ago" - if days > 365: return "over a year ago" - if days > 0 : return str(days ) + " days ago" if days > 1 else str(days ) + " day ago" - if hours > 0 : return str(hours) + " hours ago" if hours > 1 else str(hours) + " hour ago" - if mins > 0 : return str(mins ) + " minutes ago" if mins > 1 else str(mins ) + " minute ago" - return str(secs ) + " seconds ago" if secs >= 1 or secs == 0 else str(secs ) + " second ago" + if days > 730: + return "in more than two years" + if days > 365: + return "in more than a year" + if days > 0: + return f"in {days} days" if days > 1 else f"in {days} day" + if hours > 0: + return f"in {hours} hours" if hours > 1 else f"in {hours} hour" + if mins > 0: + return f"in {mins} minutes" if mins > 1 else f"in {mins} minute" + return f"in {secs} seconds" if secs >= 1 or secs == 0 else f"in {secs} second" -def text_color_duration(duration): - """ Prints a color based on duratioin (imported as seconds) """ + if days > 730: + return "over two years ago" + if days > 365: + return "over a year ago" + if days > 0: + return f"{days} days ago" if days > 1 else f"{days} day ago" + if hours > 0: + return f"{hours} hours ago" if hours > 1 else f"{hours} hour ago" + if mins > 0: + return f"{mins} minutes ago" if mins > 1 else f"{mins} minute ago" + return f"{secs} seconds ago" if secs >= 1 or secs == 0 else f"{secs} second ago" + +def text_color_duration( + duration: timedelta, +): # pylint: disable=too-many-return-statements + """Print a color based on duration (imported as seconds).""" days, seconds = duration.days, duration.seconds - hours = (days * 24 + seconds // 3600) - mins = ((seconds % 3600) // 60) - secs = (seconds % 60) - if days > 30: return "grey-text " - if days > 14: return "red-text text-darken-2 " - if days > 5: return "deep-orange-text text-lighten-1" - if days > 1: return "deep-orange-text text-lighten-1" - if hours > 12: return "orange-text " - if hours > 1: return "orange-text text-lighten-2" - if hours == 1: return "yellow-text " - if mins > 15: return "yellow-text text-lighten-2" - if mins > 5: return "green-text text-lighten-3" - if secs > 30: return "green-text text-lighten-2" + hours = days * 24 + seconds // 3600 + mins = (seconds % 3600) // 60 + secs = seconds % 60 + if days > 30: + return "grey-text " + if days > 14: + return "red-text text-darken-2 " + if days > 5: + return "deep-orange-text text-lighten-1" + if days > 1: + return "deep-orange-text text-lighten-1" + if hours > 12: + return "orange-text " + if hours > 1: + return "orange-text text-lighten-2" + if hours == 1: + return "yellow-text " + if mins > 15: + return "yellow-text text-lighten-2" + if mins > 5: + return "green-text text-lighten-3" + if secs > 30: + return "green-text text-lighten-2" return "green-text " -def key_check(): - """ Checks the validity of a Headsclae API key and renews it if it's nearing expiration """ - api_key = headscale.get_api_key() - url = headscale.get_url() - # Test the API key. If the test fails, return a failure. - # AKA, if headscale returns Unauthorized, fail: - app.logger.info("Testing API key validity.") - status = headscale.test_api_key(url, api_key) - if status != 200: - app.logger.info("Got a non-200 response from Headscale. Test failed (Response: %i)", status) - return False - else: - app.logger.info("Key check passed.") - # Check if the key needs to be renewed - headscale.renew_api_key(url, api_key) - return True - -def get_color(import_id, item_type = ""): - """ Sets colors for users/namespaces """ +def get_color(import_id: int, item_type: Literal["failover", "text", ""] = ""): + """Get color for users/namespaces.""" # Define the colors... Seems like a good number to start with - if item_type == "failover": - colors = [ - "teal lighten-1", - "blue lighten-1", - "blue-grey lighten-1", - "indigo lighten-2", - "brown lighten-1", - "grey lighten-1", - "indigo lighten-2", - "deep-orange lighten-1", - "yellow lighten-2", - "purple lighten-2", - ] - index = import_id % len(colors) - return colors[index] - if item_type == "text": - colors = [ - "red-text text-lighten-1", - "teal-text text-lighten-1", - "blue-text text-lighten-1", - "blue-grey-text text-lighten-1", - "indigo-text text-lighten-2", - "green-text text-lighten-1", - "deep-orange-text text-lighten-1", - "yellow-text text-lighten-2", - "purple-text text-lighten-2", - "indigo-text text-lighten-2", - "brown-text text-lighten-1", - "grey-text text-lighten-1", - ] - index = import_id % len(colors) - return colors[index] - colors = [ - "red lighten-1", - "teal lighten-1", - "blue lighten-1", - "blue-grey lighten-1", - "indigo lighten-2", - "green lighten-1", - "deep-orange lighten-1", - "yellow lighten-2", - "purple lighten-2", - "indigo lighten-2", - "brown lighten-1", - "grey lighten-1", - ] - index = import_id % len(colors) - return colors[index] + match item_type: + case "failover": + colors = [ + "teal lighten-1", + "blue lighten-1", + "blue-grey lighten-1", + "indigo lighten-2", + "brown lighten-1", + "grey lighten-1", + "indigo lighten-2", + "deep-orange lighten-1", + "yellow lighten-2", + "purple lighten-2", + ] + case "text": + colors = [ + "red-text text-lighten-1", + "teal-text text-lighten-1", + "blue-text text-lighten-1", + "blue-grey-text text-lighten-1", + "indigo-text text-lighten-2", + "green-text text-lighten-1", + "deep-orange-text text-lighten-1", + "yellow-text text-lighten-2", + "purple-text text-lighten-2", + "indigo-text text-lighten-2", + "brown-text text-lighten-1", + "grey-text text-lighten-1", + ] + case _: + colors = [ + "red lighten-1", + "teal lighten-1", + "blue lighten-1", + "blue-grey lighten-1", + "indigo lighten-2", + "green lighten-1", + "deep-orange lighten-1", + "yellow lighten-2", + "purple lighten-2", + "indigo lighten-2", + "brown lighten-1", + "grey lighten-1", + ] + return colors[import_id % len(colors)] -def format_message(error_type, title, message): - """ Defines a generic 'collection' as error/warning/info messages """ - content = """ - - """ + WARNING = "warning" + SUCCESS = "success" + ERROR = "error" + INFORMATION = "information" + + +def format_message(error_type: MessageErrorType, title: str, message: str): + """Render a "collection" as error/warning/info message.""" + content = '" return content - -def access_checks(): - """ Checks various items before each page load to ensure permissions are correct """ - url = headscale.get_url() - - # Return an error message if things fail. - # Return a formatted error message for EACH fail. - checks_passed = True # Default to true. Set to false when any checks fail. - data_readable = False # Checks R permissions of DATA_DIRECTORY - data_writable = False # Checks W permissions of DATA_DIRECTORY - data_executable = False # Execute on directories allows file access - file_readable = False # Checks R permissions of DATA_DIRECTORY/key.txt - file_writable = False # Checks W permissions of DATA_DIRECTORY/key.txt - file_exists = False # Checks if DATA_DIRECTORY/key.txt exists - config_readable = False # Checks if the headscale configuration file is readable - - - # Check 1: Check: the Headscale server is reachable: - server_reachable = False - response = requests.get(str(url)+"/health") - if response.status_code == 200: - server_reachable = True - else: - checks_passed = False - app.logger.critical("Headscale URL: Response 200: FAILED") - - # Check: DATA_DIRECTORY is rwx for 1000:1000: - if os.access(DATA_DIRECTORY, os.R_OK): data_readable = True - else: - app.logger.critical(f"{DATA_DIRECTORY} READ: FAILED") - checks_passed = False - if os.access(DATA_DIRECTORY, os.W_OK): data_writable = True - else: - app.logger.critical(f"{DATA_DIRECTORY} WRITE: FAILED") - checks_passed = False - if os.access(DATA_DIRECTORY, os.X_OK): data_executable = True - else: - app.logger.critical(f"{DATA_DIRECTORY} EXEC: FAILED") - checks_passed = False - - # Check: DATA_DIRECTORY/key.txt exists and is rw: - if os.access(os.path.join(DATA_DIRECTORY, "key.txt"), os.F_OK): - file_exists = True - if os.access(os.path.join(DATA_DIRECTORY, "key.txt"), os.R_OK): file_readable = True - else: - app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} READ: FAILED") - checks_passed = False - if os.access(os.path.join(DATA_DIRECTORY, "key.txt"), os.W_OK): file_writable = True - else: - app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} WRITE: FAILED") - checks_passed = False - else: app.logger.error(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} EXIST: FAILED - NO ERROR") - - # Check: /etc/headscale/config.yaml is readable: - if os.access('/etc/headscale/config.yaml', os.R_OK): config_readable = True - elif os.access('/etc/headscale/config.yml', os.R_OK): config_readable = True - else: - app.logger.error("/etc/headscale/config.y(a)ml: READ: FAILED") - checks_passed = False - - if checks_passed: - app.logger.info("All startup checks passed.") - return "Pass" - - message_html = "" - # Generate the message: - if not server_reachable: - app.logger.critical("Server is unreachable") - message = """ -

Your headscale server is either unreachable or not properly configured. - Please ensure your configuration is correct (Check for 200 status on - """+url+"""/api/v1 failed. Response: """+str(response.status_code)+""".)

- """ - - message_html += format_message("Error", "Headscale unreachable", message) - - if not config_readable: - app.logger.critical("Headscale configuration is not readable") - message = """ -

/etc/headscale/config.yaml not readable. Please ensure your - headscale configuration file resides in /etc/headscale and - is named "config.yaml" or "config.yml"

- """ - - message_html += format_message("Error", "/etc/headscale/config.yaml not readable", message) - - if not data_writable: - app.logger.critical(f"{DATA_DIRECTORY} folder is not writable") - message = f""" -

{DATA_DIRECTORY} is not writable. Please ensure your - permissions are correct. {DATA_DIRECTORY} mount should be writable - by UID/GID 1000:1000.

- """ - - message_html += format_message("Error", f"{DATA_DIRECTORY} not writable", message) - - if not data_readable: - app.logger.critical(f"{DATA_DIRECTORY} folder is not readable") - message = f""" -

{DATA_DIRECTORY} is not readable. Please ensure your - permissions are correct. {DATA_DIRECTORY} mount should be readable - by UID/GID 1000:1000.

- """ - - message_html += format_message("Error", f"{DATA_DIRECTORY} not readable", message) - - if not data_executable: - app.logger.critical(f"{DATA_DIRECTORY} folder is not readable") - message = f""" -

{DATA_DIRECTORY} is not executable. Please ensure your - permissions are correct. {DATA_DIRECTORY} mount should be readable - by UID/GID 1000:1000. (chown 1000:1000 /path/to/data && chmod -R 755 /path/to/data)

- """ - - message_html += format_message("Error", f"{DATA_DIRECTORY} not executable", message) - - - if file_exists: - # If it doesn't exist, we assume the user hasn't created it yet. - # Just redirect to the settings page to enter an API Key - if not file_writable: - app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} is not writable") - message = f""" -

{os.path.join(DATA_DIRECTORY, 'key.txt')} is not writable. Please ensure your - permissions are correct. {DATA_DIRECTORY} mount should be writable - by UID/GID 1000:1000.

- """ - - message_html += format_message("Error", f"{os.path.join(DATA_DIRECTORY, 'key.txt')} not writable", message) - - if not file_readable: - app.logger.critical(f"{os.path.join(DATA_DIRECTORY, 'key.txt')} is not readable") - message = f""" -

{os.path.join(DATA_DIRECTORY, 'key.txt')} is not readable. Please ensure your - permissions are correct. {DATA_DIRECTORY} mount should be readable - by UID/GID 1000:1000.

- """ - - message_html += format_message("Error", f"{os.path.join(DATA_DIRECTORY, 'key.txt')} not readable", message) - - return message_html - -def load_checks(): - """ Bundles all the checks into a single function to call easier """ - # General error checks. See the function for more info: - if access_checks() != "Pass": return 'error_page' - # If the API key fails, redirect to the settings page: - if not key_check(): return 'settings_page' - return "Pass" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..99f7650 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2267 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "aiohttp" +version = "3.8.4" +description = "Async http client/server framework (asyncio)" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1"}, + {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a"}, + {file = "aiohttp-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea"}, + {file = "aiohttp-3.8.4-cp310-cp310-win32.whl", hash = "sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1"}, + {file = "aiohttp-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24"}, + {file = "aiohttp-3.8.4-cp311-cp311-win32.whl", hash = "sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d"}, + {file = "aiohttp-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc"}, + {file = "aiohttp-3.8.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff"}, + {file = "aiohttp-3.8.4-cp36-cp36m-win32.whl", hash = "sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777"}, + {file = "aiohttp-3.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e"}, + {file = "aiohttp-3.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241"}, + {file = "aiohttp-3.8.4-cp37-cp37m-win32.whl", hash = "sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a"}, + {file = "aiohttp-3.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d"}, + {file = "aiohttp-3.8.4-cp38-cp38-win32.whl", hash = "sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54"}, + {file = "aiohttp-3.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4"}, + {file = "aiohttp-3.8.4-cp39-cp39-win32.whl", hash = "sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a"}, + {file = "aiohttp-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04"}, + {file = "aiohttp-3.8.4.tar.gz", hash = "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" +attrs = ">=17.3.0" +charset-normalizer = ">=2.0,<4.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "cchardet"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "apscheduler" +version = "3.10.1" +description = "In-process task scheduler with Cron-like capabilities" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "APScheduler-3.10.1-py3-none-any.whl", hash = "sha256:e813ad5ada7aff36fb08cdda746b520531eaac7757832abc204868ba78e0c8f6"}, + {file = "APScheduler-3.10.1.tar.gz", hash = "sha256:0293937d8f6051a0f493359440c1a1b93e882c57daf0197afeff0e727777b96e"}, +] + +[package.dependencies] +pytz = "*" +setuptools = ">=0.7" +six = ">=1.4.0" +tzlocal = ">=2.0,<3.0.0 || >=4.0.0" + +[package.extras] +doc = ["sphinx", "sphinx-rtd-theme"] +gevent = ["gevent"] +mongodb = ["pymongo (>=3.0)"] +redis = ["redis (>=3.0)"] +rethinkdb = ["rethinkdb (>=2.4.0)"] +sqlalchemy = ["sqlalchemy (>=1.4)"] +testing = ["pytest", "pytest-asyncio", "pytest-cov", "pytest-tornado5"] +tornado = ["tornado (>=4.3)"] +twisted = ["twisted"] +zookeeper = ["kazoo"] + +[[package]] +name = "asgiref" +version = "3.6.0" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgiref-3.6.0-py3-none-any.whl", hash = "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac"}, + {file = "asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "astroid" +version = "2.15.3" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "astroid-2.15.3-py3-none-any.whl", hash = "sha256:f11e74658da0f2a14a8d19776a8647900870a63de71db83713a8e77a6af52662"}, + {file = "astroid-2.15.3.tar.gz", hash = "sha256:44224ad27c54d770233751315fa7f74c46fa3ee0fab7beef1065f99f09897efe"}, +] + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +wrapt = {version = ">=1.14,<2", markers = "python_version >= \"3.11\""} + +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "betterproto" +version = "2.0.0b5" +description = "A better Protobuf / gRPC generator & library" +category = "main" +optional = false +python-versions = "^3.7" +files = [] +develop = false + +[package.dependencies] +black = {version = ">=19.3b0", optional = true} +grpclib = "^0.4.1" +isort = {version = "^5.11.5", optional = true} +jinja2 = {version = ">=3.0.3", optional = true} +python-dateutil = "^2.8" + +[package.extras] +compiler = ["black (>=19.3b0)", "isort (>=5.11.5,<6.0.0)", "jinja2 (>=3.0.3)"] + +[package.source] +type = "git" +url = "https://github.com/MarekPikula/python-betterproto.git" +reference = "classmethod_from_dict" +resolved_reference = "d7929e9b302697d28cf661f9182f80d201facb18" + +[[package]] +name = "black" +version = "23.3.0" +description = "The uncompromising code formatter." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.1.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, +] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.2.3" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"}, + {file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"}, + {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"}, + {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"}, + {file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"}, + {file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"}, + {file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"}, + {file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"}, + {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"}, + {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"}, + {file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"}, + {file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"}, + {file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"}, + {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"}, + {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"}, + {file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"}, + {file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"}, + {file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"}, + {file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"}, + {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"}, + {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"}, + {file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"}, + {file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"}, + {file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"}, + {file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"}, + {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"}, + {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"}, + {file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"}, + {file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"}, + {file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"}, + {file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "39.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "cryptography-39.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:2725672bb53bb92dc7b4150d233cd4b8c59615cd8288d495eaa86db00d4e5c06"}, + {file = "cryptography-39.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:23df8ca3f24699167daf3e23e51f7ba7334d504af63a94af468f468b975b7dd7"}, + {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:eb40fe69cfc6f5cdab9a5ebd022131ba21453cf7b8a7fd3631f45bbf52bed612"}, + {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc0521cce2c1d541634b19f3ac661d7a64f9555135e9d8af3980965be717fd4a"}, + {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffd394c7896ed7821a6d13b24657c6a34b6e2650bd84ae063cf11ccffa4f1a97"}, + {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:e8a0772016feeb106efd28d4a328e77dc2edae84dfbac06061319fdb669ff828"}, + {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8f35c17bd4faed2bc7797d2a66cbb4f986242ce2e30340ab832e5d99ae60e011"}, + {file = "cryptography-39.0.2-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b49a88ff802e1993b7f749b1eeb31134f03c8d5c956e3c125c75558955cda536"}, + {file = "cryptography-39.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c682e736513db7d04349b4f6693690170f95aac449c56f97415c6980edef5"}, + {file = "cryptography-39.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:d7d84a512a59f4412ca8549b01f94be4161c94efc598bf09d027d67826beddc0"}, + {file = "cryptography-39.0.2-cp36-abi3-win32.whl", hash = "sha256:c43ac224aabcbf83a947eeb8b17eaf1547bce3767ee2d70093b461f31729a480"}, + {file = "cryptography-39.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:788b3921d763ee35dfdb04248d0e3de11e3ca8eb22e2e48fef880c42e1f3c8f9"}, + {file = "cryptography-39.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d15809e0dbdad486f4ad0979753518f47980020b7a34e9fc56e8be4f60702fac"}, + {file = "cryptography-39.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:50cadb9b2f961757e712a9737ef33d89b8190c3ea34d0fb6675e00edbe35d074"}, + {file = "cryptography-39.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:103e8f7155f3ce2ffa0049fe60169878d47a4364b277906386f8de21c9234aa1"}, + {file = "cryptography-39.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6236a9610c912b129610eb1a274bdc1350b5df834d124fa84729ebeaf7da42c3"}, + {file = "cryptography-39.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e944fe07b6f229f4c1a06a7ef906a19652bdd9fd54c761b0ff87e83ae7a30354"}, + {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:35d658536b0a4117c885728d1a7032bdc9a5974722ae298d6c533755a6ee3915"}, + {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:30b1d1bfd00f6fc80d11300a29f1d8ab2b8d9febb6ed4a38a76880ec564fae84"}, + {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e029b844c21116564b8b61216befabca4b500e6816fa9f0ba49527653cae2108"}, + {file = "cryptography-39.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fa507318e427169ade4e9eccef39e9011cdc19534f55ca2f36ec3f388c1f70f3"}, + {file = "cryptography-39.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8bc0008ef798231fac03fe7d26e82d601d15bd16f3afaad1c6113771566570f3"}, + {file = "cryptography-39.0.2.tar.gz", hash = "sha256:bc5b871e977c8ee5a1bbc42fa8d19bcc08baf0c51cbf1586b0e87a2694dde42f"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"] +test-randomorder = ["pytest-randomly"] +tox = ["tox"] + +[[package]] +name = "deprecated" +version = "1.2.13" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] + +[[package]] +name = "dill" +version = "0.3.6" +description = "serialize all of python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, + {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "distlib" +version = "0.3.6" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] + +[[package]] +name = "filelock" +version = "3.12.0" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"}, + {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"}, +] + +[package.extras] +docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "flask" +version = "2.2.3" +description = "A simple framework for building complex web applications." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Flask-2.2.3-py3-none-any.whl", hash = "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d"}, + {file = "Flask-2.2.3.tar.gz", hash = "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d"}, +] + +[package.dependencies] +asgiref = {version = ">=3.2", optional = true, markers = "extra == \"async\""} +click = ">=8.0" +itsdangerous = ">=2.0" +Jinja2 = ">=3.0" +Werkzeug = ">=2.2.2" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-basicauth" +version = "0.2.0" +description = "HTTP basic access authentication for Flask." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "Flask-BasicAuth-0.2.0.tar.gz", hash = "sha256:df5ebd489dc0914c224419da059d991eb72988a01cdd4b956d52932ce7d501ff"}, +] + +[package.dependencies] +Flask = "*" + +[[package]] +name = "flask-providers-oidc" +version = "1.2.1" +description = "Fork version flask oidc" +category = "main" +optional = false +python-versions = ">=3.7.2,<4.0.0" +files = [ + {file = "flask_providers_oidc-1.2.1-py3-none-any.whl", hash = "sha256:f77c600589f03d027c086a66d60e577230db61955b90f7a59d16a62210b43e06"}, + {file = "flask_providers_oidc-1.2.1.tar.gz", hash = "sha256:c952d7f653f529ebe46d0de51063aff585dac5b7087aabc3d102210dd55e26c7"}, +] + +[package.dependencies] +oauth2client = ">=4.1.3,<5.0.0" +PyJWT = ">=2.6.0,<3.0.0" + +[[package]] +name = "Flask-Pydantic" +version = "0.11.0" +description = "Flask extension for integration with Pydantic library" +category = "main" +optional = false +python-versions = ">=3.6" +files = [] +develop = false + +[package.dependencies] +Flask = "*" +pydantic = ">=1.7" +typing-extensions = ">=4.1.1" + +[package.source] +type = "git" +url = "https://github.com/MarekPikula/flask-pydantic.git" +reference = "dictable_models" +resolved_reference = "b85358318fb600f00ca8891437d573809d0c61b4" + +[[package]] +name = "frozenlist" +version = "1.3.3" +description = "A list-like structure which implements collections.abc.MutableSequence" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"}, + {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"}, + {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"}, + {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"}, + {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"}, + {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"}, + {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"}, + {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"}, + {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"}, + {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, + {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, +] + +[[package]] +name = "gitdb" +version = "4.0.10" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, + {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.31" +description = "GitPython is a Python library used to interact with Git repositories" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "GitPython-3.1.31-py3-none-any.whl", hash = "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d"}, + {file = "GitPython-3.1.31.tar.gz", hash = "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[[package]] +name = "grpclib" +version = "0.4.3" +description = "Pure-Python gRPC implementation for asyncio" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "grpclib-0.4.3.tar.gz", hash = "sha256:eadf2002fc5a25158b707c0338a6c0b96dd7fbdc6df66f7e515e7f041d56a940"}, +] + +[package.dependencies] +h2 = ">=3.1.0,<5" +multidict = "*" + +[package.extras] +protobuf = ["protobuf (>=3.15.0)"] + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] + +[package.dependencies] +setuptools = ">=3.0" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "h2" +version = "4.1.0" +description = "HTTP/2 State-Machine based protocol implementation" +category = "main" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] + +[package.dependencies] +hpack = ">=4.0,<5" +hyperframe = ">=6.0,<7" + +[[package]] +name = "headscale-api" +version = "0.2.0" +description = "Python Headscale API and configuration abstraction." +category = "main" +optional = false +python-versions = "^3.11" # TODO: Change to 3.7 once datetime parsing is fixed. +files = [] +develop = false + +[package.dependencies] +aiohttp = "^3.8.4" +betterproto = {version = "2.0.0b5", extras = ["compiler"]} +pydantic = "^1.10.7" +pydantic-yaml = {version = "^0.11.2", extras = ["ruamel"]} + +[package.source] +type = "git" +url = "https://github.com/MarekPikula/python-headscale-api.git" +reference = "HEAD" +resolved_reference = "ea01ea4ce22b82fb9f2a58855dfee68e72cdef02" + +[[package]] +name = "hpack" +version = "4.0.0" +description = "Pure-Python HPACK header compression" +category = "main" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, +] + +[[package]] +name = "httplib2" +version = "0.22.0" +description = "A comprehensive HTTP client library." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, + {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, +] + +[package.dependencies] +pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} + +[[package]] +name = "hyperframe" +version = "6.0.1" +description = "HTTP/2 framing layer for Python" +category = "main" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, + {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, +] + +[[package]] +name = "identify" +version = "2.5.22" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"}, + {file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.5.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-6.5.0-py3-none-any.whl", hash = "sha256:03ba783c3a2c69d751b109fc0c94a62c51f581b3d6acf8ed1331b6d5729321ff"}, + {file = "importlib_metadata-6.5.0.tar.gz", hash = "sha256:7a8bdf1bc3a726297f5cfbc999e6e7ff6b4fa41b26bba4afc580448624460045"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flake8 (<5)", "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-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +category = "main" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "itsdangerous" +version = "2.1.2" +description = "Safely pass data to untrusted environments and back." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, + {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "lazy-object-proxy" +version = "1.9.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, + {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"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, + {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"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, + {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"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, + {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"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, + {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"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "multidict" +version = "6.0.4" +description = "multidict implementation" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] + +[[package]] +name = "mypy" +version = "1.2.0" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:701189408b460a2ff42b984e6bd45c3f41f0ac9f5f58b8873bbedc511900086d"}, + {file = "mypy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe91be1c51c90e2afe6827601ca14353bbf3953f343c2129fa1e247d55fd95ba"}, + {file = "mypy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d26b513225ffd3eacece727f4387bdce6469192ef029ca9dd469940158bc89e"}, + {file = "mypy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3a2d219775a120581a0ae8ca392b31f238d452729adbcb6892fa89688cb8306a"}, + {file = "mypy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2e93a8a553e0394b26c4ca683923b85a69f7ccdc0139e6acd1354cc884fe0128"}, + {file = "mypy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3efde4af6f2d3ccf58ae825495dbb8d74abd6d176ee686ce2ab19bd025273f41"}, + {file = "mypy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:695c45cea7e8abb6f088a34a6034b1d273122e5530aeebb9c09626cea6dca4cb"}, + {file = "mypy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0e9464a0af6715852267bf29c9553e4555b61f5904a4fc538547a4d67617937"}, + {file = "mypy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8293a216e902ac12779eb7a08f2bc39ec6c878d7c6025aa59464e0c4c16f7eb9"}, + {file = "mypy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f46af8d162f3d470d8ffc997aaf7a269996d205f9d746124a179d3abe05ac602"}, + {file = "mypy-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:031fc69c9a7e12bcc5660b74122ed84b3f1c505e762cc4296884096c6d8ee140"}, + {file = "mypy-1.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:390bc685ec209ada4e9d35068ac6988c60160b2b703072d2850457b62499e336"}, + {file = "mypy-1.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4b41412df69ec06ab141808d12e0bf2823717b1c363bd77b4c0820feaa37249e"}, + {file = "mypy-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4e4a682b3f2489d218751981639cffc4e281d548f9d517addfd5a2917ac78119"}, + {file = "mypy-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a197ad3a774f8e74f21e428f0de7f60ad26a8d23437b69638aac2764d1e06a6a"}, + {file = "mypy-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9a084bce1061e55cdc0493a2ad890375af359c766b8ac311ac8120d3a472950"}, + {file = "mypy-1.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaeaa0888b7f3ccb7bcd40b50497ca30923dba14f385bde4af78fac713d6d6f6"}, + {file = "mypy-1.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bea55fc25b96c53affab852ad94bf111a3083bc1d8b0c76a61dd101d8a388cf5"}, + {file = "mypy-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:4c8d8c6b80aa4a1689f2a179d31d86ae1367ea4a12855cc13aa3ba24bb36b2d8"}, + {file = "mypy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70894c5345bea98321a2fe84df35f43ee7bb0feec117a71420c60459fc3e1eed"}, + {file = "mypy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4a99fe1768925e4a139aace8f3fb66db3576ee1c30b9c0f70f744ead7e329c9f"}, + {file = "mypy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023fe9e618182ca6317ae89833ba422c411469156b690fde6a315ad10695a521"}, + {file = "mypy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d19f1a239d59f10fdc31263d48b7937c585810288376671eaf75380b074f238"}, + {file = "mypy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:2de7babe398cb7a85ac7f1fd5c42f396c215ab3eff731b4d761d68d0f6a80f48"}, + {file = "mypy-1.2.0-py3-none-any.whl", hash = "sha256:d8e9187bfcd5ffedbe87403195e1fc340189a68463903c39e2b63307c9fa0394"}, + {file = "mypy-1.2.0.tar.gz", hash = "sha256:f70a40410d774ae23fcb4afbbeca652905a04de7948eaf0b1789c8d1426b72d1"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "oauth2client" +version = "4.1.3" +description = "OAuth 2.0 client library" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "oauth2client-4.1.3-py2.py3-none-any.whl", hash = "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac"}, + {file = "oauth2client-4.1.3.tar.gz", hash = "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6"}, +] + +[package.dependencies] +httplib2 = ">=0.9.1" +pyasn1 = ">=0.1.7" +pyasn1-modules = ">=0.0.5" +rsa = ">=3.1.4" +six = ">=1.6.1" + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, + {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, +] + +[[package]] +name = "platformdirs" +version = "3.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, + {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pre-commit" +version = "3.2.2" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pre_commit-3.2.2-py2.py3-none-any.whl", hash = "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4"}, + {file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pyasn1" +version = "0.5.0" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, + {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.3.0" +description = "A collection of ASN.1-based protocols modules" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, + {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.6.0" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pydantic" +version = "1.10.7" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, + {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, + {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, + {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, + {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, + {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, + {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, + {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, + {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, + {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pydantic-yaml" +version = "0.11.2" +description = "Adds some YAML functionality to the excellent `pydantic` library." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_yaml-0.11.2-py3-none-any.whl", hash = "sha256:0f70235472861985eaca3fe6c71d86329556c296052ac522c5ebc7322e0749f3"}, + {file = "pydantic_yaml-0.11.2.tar.gz", hash = "sha256:19c8f3c9a97041b0a3d8fc06ca5143ff71c0846c45b39fde719cfbc98be7a00c"}, +] + +[package.dependencies] +deprecated = ">=1.2.5,<1.3.0" +importlib-metadata = "*" +pydantic = ">=1.8,<2" +"ruamel.yaml" = {version = ">=0.15,<0.18", optional = true, markers = "extra == \"ruamel\""} +types-Deprecated = "*" + +[package.extras] +dev = ["black (==23.3.0)", "flake8", "mypy (==1.0.0)", "pre-commit (==2.21.0)", "pytest (==7.2.2)", "setuptools (>=61.0.0)", "setuptools-scm[toml] (>=6.2)"] +docs = ["mkdocs", "mkdocs-material", "mkdocstrings[python]", "pygments", "pymdown-extensions"] +pyyaml = ["pyyaml", "types-PyYAML"] +ruamel = ["ruamel.yaml (>=0.15,<0.18)"] +semver = ["semver (>=2.13.0,<4)"] + +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3)"] + +[[package]] +name = "pyjwt" +version = "2.6.0" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"}, + {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pylint" +version = "2.17.2" +description = "python code static checker" +category = "dev" +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "pylint-2.17.2-py3-none-any.whl", hash = "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8"}, + {file = "pylint-2.17.2.tar.gz", hash = "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"}, +] + +[package.dependencies] +astroid = ">=2.15.2,<=2.17.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = {version = ">=0.3.6", markers = "python_version >= \"3.11\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pylint-plugin-utils" +version = "0.7" +description = "Utilities and helpers for writing Pylint plugins" +category = "dev" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "pylint-plugin-utils-0.7.tar.gz", hash = "sha256:ce48bc0516ae9415dd5c752c940dfe601b18fe0f48aa249f2386adfa95a004dd"}, + {file = "pylint_plugin_utils-0.7-py3-none-any.whl", hash = "sha256:b3d43e85ab74c4f48bb46ae4ce771e39c3a20f8b3d56982ab17aa73b4f98d535"}, +] + +[package.dependencies] +pylint = ">=1.7" + +[[package]] +name = "pylint-pydantic" +version = "0.1.8" +description = "A Pylint plugin to help Pylint understand the Pydantic" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pylint_pydantic-0.1.8-py3-none-any.whl", hash = "sha256:4033c67e06885115fa3bb16e3b9ce918ac6439a87e9b4d314158e09bc1067ecb"}, +] + +[package.dependencies] +pydantic = "<2.0" +pylint = ">2.0,<3.0" +pylint-plugin-utils = "*" + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + +[[package]] +name = "pytz-deprecation-shim" +version = "0.1.0.post0" +description = "Shims to make deprecation of pytz easier" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pytz_deprecation_shim-0.1.0.post0-py2.py3-none-any.whl", hash = "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6"}, + {file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "python_version >= \"3.6\""} + +[[package]] +name = "pyuwsgi" +version = "2.0.21" +description = "The uWSGI server" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pyuwsgi-2.0.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10cce470f3db6e5206c3fb9d46b86c5c915dcb6616a617101411006463e833ea"}, + {file = "pyuwsgi-2.0.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e06b41ad50b8d3d5a46374af8c8ed9bcf2627ea97f5718ef2da693ab3425656"}, + {file = "pyuwsgi-2.0.21-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:977ce0f87e3f07571267b6572dcbe8b3d5d488cbc351d33c93ec6cce9737099a"}, + {file = "pyuwsgi-2.0.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:243de3964aa40e9f15cc4be64bf5594bb4d3e847f9b563b3d8f3b2df9c1c1581"}, + {file = "pyuwsgi-2.0.21-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e29aca1e856315b18999e6527347cf461f7b333af13b33ba5926e2718c0a3c"}, + {file = "pyuwsgi-2.0.21-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0a6209bf09e14d3ceee1db6d1381346c361245552307388a1cf65229d33d306c"}, + {file = "pyuwsgi-2.0.21-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09942a86c5501367381b86561dcb69efa4207e1f604a4c5c4e58849f0b895619"}, + {file = "pyuwsgi-2.0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7139eb6bdcb32b64431ba5d3058975d6a34cc52d58c2ffbf611625cd058018a7"}, + {file = "pyuwsgi-2.0.21-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:871d0b9a61a143da0b0ba4a7249d198c804ad63a2374b5bccae7c584d805bdcd"}, + {file = "pyuwsgi-2.0.21-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78c3aaf8e89ee912730ad57e60832c0d10a267b521715c8d832eef19373075aa"}, + {file = "pyuwsgi-2.0.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8da171f2519739caad4bf4682a71b92527489eb71b3af41319bbc13f61e14dc"}, + {file = "pyuwsgi-2.0.21-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c9b67d7211e5d9439d1ecc11cf909fc214d05c332e47121d5f92913ebdf5c28c"}, + {file = "pyuwsgi-2.0.21-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c9fdd5032bd4a5d697ccfb50e2e5296c419eb53a8b44cacfcc55d7ceb629be2f"}, + {file = "pyuwsgi-2.0.21-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:445998892e164e7f253d20ffd1ab6f7c9441c77e8d05e8a2525532ba663de0af"}, + {file = "pyuwsgi-2.0.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f76540fbcea52d333acfe172c7c91f284c4526eae8b0d146c60672dbcaece705"}, + {file = "pyuwsgi-2.0.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32eace989380b3677131fea2d5e719a870fcecb2a1db5830d80997e9f501c6db"}, + {file = "pyuwsgi-2.0.21-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26876ce2e934e004d1d98a06abb170743ec743a5cecc3867260f071f31c269e0"}, + {file = "pyuwsgi-2.0.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e77e27ce32b64b34de26a2ec84cb8fff620153d7a207ea3cbea69b39c0b571b1"}, + {file = "pyuwsgi-2.0.21-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:62d9927a1178af61285b697caa736dfa34fcc48090db45f965859e1fa641f4bc"}, + {file = "pyuwsgi-2.0.21-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ed867fa6d704338820304bd13bc6b20687e823ef70dfaf35c1db324598b60af4"}, + {file = "pyuwsgi-2.0.21-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c38f1e68db1dea7e8b47a64b855cd15e491d1920908be5887189a98ce5e968e9"}, + {file = "pyuwsgi-2.0.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be994a0cdaaf9733e00c2e3275b368586db067802cd0a1af682b0c55070f39c7"}, + {file = "pyuwsgi-2.0.21-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0753b7ec6174afa590bd5724d25541209387b67f080dbe13db7d9655ef0077df"}, + {file = "pyuwsgi-2.0.21-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c17abbbba53eabdba7fc92a0321864adbf97e8460cfd9c01b714d6c3e3ccc4c"}, + {file = "pyuwsgi-2.0.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13524cea71a1cd2bb4586c773cbf6a9a1085f3e4ba1c52648b2823385c8d7d74"}, + {file = "pyuwsgi-2.0.21-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f5b6c6d413430015f9cbadef2687ce334b2960d0df3cfba4181e39c4af242933"}, + {file = "pyuwsgi-2.0.21-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1f44454a0cf419436c0a99bd37586ce9776e3c10454dd3387d2afa9c4c9c4404"}, + {file = "pyuwsgi-2.0.21-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7375666a7dd22f1c9ad4c7d01e957c5941baa489f02cd76cf2064a63a8946dfe"}, + {file = "pyuwsgi-2.0.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56f92e057461bcd32e991661db3fb505a59b8ff35da5af12b062b9ebf1ddfc57"}, + {file = "pyuwsgi-2.0.21-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c005b1357c525fe63dfb83299ca77f478db4842ee8204e8ac6cb47267fc1920"}, + {file = "pyuwsgi-2.0.21-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10d36e255bd54e7d52ed7dc360821b06748e87a7d5aa826a48ddfa7c9baedb52"}, + {file = "pyuwsgi-2.0.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8693e2a71da6e5f78fa4142893c1b201daadaf71d87275fd906ce6d02e3c9910"}, + {file = "pyuwsgi-2.0.21-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e448bdd8fbacbd27dcdd58f1191ee61c58795a672cedc4ca661a8b83606a158a"}, + {file = "pyuwsgi-2.0.21-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:837d295d3df3d4b6e1a9850922e25fc7a8836949f424a36985491be473f00d21"}, + {file = "pyuwsgi-2.0.21-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92371cb638707574fe93127f9c270216d12751a96e6a624237820a089f846ef5"}, + {file = "pyuwsgi-2.0.21.tar.gz", hash = "sha256:211e8877f5191e347ba905232d04ab30e05ce31ba7a6dac4bfcb48de9845bb52"}, +] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "requests" +version = "2.28.2" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +category = "main" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "ruamel-yaml" +version = "0.17.21" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "main" +optional = false +python-versions = ">=3" +files = [ + {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, + {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, +] + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruff" +version = "0.0.260" +description = "An extremely fast Python linter, written in Rust." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.0.260-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:c559650b623f3fbdc39c7ed1bcb064765c666a53ee738c53d1461afbf3f23db2"}, + {file = "ruff-0.0.260-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:90ff1479e292a84c388a8a035d223247ddeea5f6760752a9142b88b6d59ac334"}, + {file = "ruff-0.0.260-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25584d1b9f445fde72651caab97e7430a4c5bfd2a0ce9af39868753826cba10d"}, + {file = "ruff-0.0.260-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8032e35357384a29791c75194a71e314031171eb0731fcaa872dfaf4c1f4470a"}, + {file = "ruff-0.0.260-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4fa7293f97c021825b3b72f2bf53f0eb4f59625608a889678c1fc6660f412d"}, + {file = "ruff-0.0.260-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8bec0271e2c8cd36bcf915cb9f6a93e40797a3ff3d2cda4ca87b7bed9e598472"}, + {file = "ruff-0.0.260-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e075a61aaff8ebe56172217f0ac14c5df9637b289bf161ac697445a9003d5c2"}, + {file = "ruff-0.0.260-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8678f54eb2696481618902a10c3cb28325f3323799af99997ad6f06005ea4f5"}, + {file = "ruff-0.0.260-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57d9f0bfdef739b76aa3112b9182a214f0f34589a2659f88353492c7670fe2fe"}, + {file = "ruff-0.0.260-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ec1f77219ba5adaa194289cb82ba924ff2ed931fd00b8541d66a1724c89fbc9"}, + {file = "ruff-0.0.260-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aae2170a7ec6f7fc4a73db30aa7aa7fce936176bf66bf85f77f69ddd1dd4a665"}, + {file = "ruff-0.0.260-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f847b72ef994ab88e9da250c7eb5cbb3f1555b92a9f22c5ed1c27a44b7e98d6"}, + {file = "ruff-0.0.260-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6dd705d4eff405c2b70513188fbff520f49db6df91f0d5e8258c5d469efa58bc"}, + {file = "ruff-0.0.260-py3-none-win32.whl", hash = "sha256:3866a96b2ef92c7d837ba6bf8fc9dd125a67886f1c5512ad6fa5d5fefaceff87"}, + {file = "ruff-0.0.260-py3-none-win_amd64.whl", hash = "sha256:0733d524946decbd4f1e63f7dc26820f5c1e6c31da529ba20fb995057f8e79b1"}, + {file = "ruff-0.0.260-py3-none-win_arm64.whl", hash = "sha256:12542a26f189a5a10c719bfa14d415d0511ac05e5c9ff5e79cc9d5cc50b81bc8"}, + {file = "ruff-0.0.260.tar.gz", hash = "sha256:ea8f94262f33b81c47ee9d81f455b144e94776f5c925748cb0c561a12206eae1"}, +] + +[[package]] +name = "setuptools" +version = "67.7.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-67.7.0-py3-none-any.whl", hash = "sha256:888be97fde8cc3afd60f7784e678fa29ee13c4e5362daa7104a93bba33646c50"}, + {file = "setuptools-67.7.0.tar.gz", hash = "sha256:b7e53a01c6c654d26d2999ee033d8c6125e5fa55f03b7b193f937ae7ac999f22"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, + {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "tomlkit" +version = "0.11.7" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"}, + {file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"}, +] + +[[package]] +name = "types-deprecated" +version = "1.2.9.2" +description = "Typing stubs for Deprecated" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "types-Deprecated-1.2.9.2.tar.gz", hash = "sha256:91616fd6745f8bf2d457fbbbefd14cde43838e9f00a04b5a0eae4fc1f7bbc697"}, + {file = "types_Deprecated-1.2.9.2-py3-none-any.whl", hash = "sha256:327783e137353b0ef9cf47a8cd4b1c0b8ae72f6554eb25820783c6a81a3d556f"}, +] + +[[package]] +name = "types-requests" +version = "2.28.11.17" +description = "Typing stubs for requests" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-requests-2.28.11.17.tar.gz", hash = "sha256:0d580652ce903f643f8c3b494dd01d29367ea57cea0c7ad7f65cf3169092edb0"}, + {file = "types_requests-2.28.11.17-py3-none-any.whl", hash = "sha256:cc1aba862575019306b2ed134eb1ea994cab1c887a22e18d3383e6dd42e9789b"}, +] + +[package.dependencies] +types-urllib3 = "<1.27" + +[[package]] +name = "types-urllib3" +version = "1.26.25.10" +description = "Typing stubs for urllib3" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-urllib3-1.26.25.10.tar.gz", hash = "sha256:c44881cde9fc8256d05ad6b21f50c4681eb20092552351570ab0a8a0653286d6"}, + {file = "types_urllib3-1.26.25.10-py3-none-any.whl", hash = "sha256:12c744609d588340a07e45d333bf870069fc8793bcf96bae7a96d4712a42591d"}, +] + +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "tzlocal" +version = "4.3" +description = "tzinfo object for the local timezone" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tzlocal-4.3-py3-none-any.whl", hash = "sha256:b44c4388f3d34f25862cfbb387578a4d70fec417649da694a132f628a23367e2"}, + {file = "tzlocal-4.3.tar.gz", hash = "sha256:3f21d09e1b2aa9f2dacca12da240ca37de3ba5237a93addfd6d593afe9073355"}, +] + +[package.dependencies] +pytz-deprecation-shim = "*" +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + +[[package]] +name = "urllib3" +version = "1.26.15" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, + {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "virtualenv" +version = "20.22.0" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.22.0-py3-none-any.whl", hash = "sha256:48fd3b907b5149c5aab7c23d9790bea4cac6bc6b150af8635febc4cfeab1275a"}, + {file = "virtualenv-20.22.0.tar.gz", hash = "sha256:278753c47aaef1a0f14e6db8a4c5e1e040e90aea654d0fc1dc7e0d8a42616cc3"}, +] + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.11,<4" +platformdirs = ">=3.2,<4" + +[package.extras] +docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "werkzeug" +version = "2.2.3" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Werkzeug-2.2.3-py3-none-any.whl", hash = "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"}, + {file = "Werkzeug-2.2.3.tar.gz", hash = "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog"] + +[[package]] +name = "wrapt" +version = "1.15.0" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +] + +[[package]] +name = "yarl" +version = "1.8.2" +description = "Yet another URL library" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5"}, + {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863"}, + {file = "yarl-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8"}, + {file = "yarl-1.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3"}, + {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80"}, + {file = "yarl-1.8.2-cp310-cp310-win32.whl", hash = "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42"}, + {file = "yarl-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd"}, + {file = "yarl-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76"}, + {file = "yarl-1.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c"}, + {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2"}, + {file = "yarl-1.8.2-cp311-cp311-win32.whl", hash = "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b"}, + {file = "yarl-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c"}, + {file = "yarl-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89"}, + {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7"}, + {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37"}, + {file = "yarl-1.8.2-cp37-cp37m-win32.whl", hash = "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89"}, + {file = "yarl-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918"}, + {file = "yarl-1.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3"}, + {file = "yarl-1.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c"}, + {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946"}, + {file = "yarl-1.8.2-cp38-cp38-win32.whl", hash = "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165"}, + {file = "yarl-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf"}, + {file = "yarl-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08"}, + {file = "yarl-1.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516"}, + {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588"}, + {file = "yarl-1.8.2-cp39-cp39-win32.whl", hash = "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83"}, + {file = "yarl-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778"}, + {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "eb19a63136fcae2fc1b0fcf840bf3c66329c6c27d8112c3908bae260dd5cc93e" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml index c0619d2..6e61cbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,47 @@ [tool.poetry] name = "headscale-webui" -version = "v0.6.2" +version = "v0.7.0" description = "A simple web UI for small-scale Headscale deployments." authors = ["Albert Copeland "] license = "AGPL" +readme = "README.md" +repository = "https://github.com/iFargle/headscale-webui" [tool.poetry.dependencies] python = "^3.11" requests = "^2.28.2" -Flask = "^2.2.2" +Flask = {extras = ["async"], version = "^2.2.3"} cryptography = "^39.0.0" -python-dateutil = "^2.8.2" -pytz = "^2022.7.1" -Flask-Executor = "^1.0.0" -PyYAML = "^6.0" pyuwsgi = "^2.0.21" gunicorn = "^20.1.0" flask-basicauth = "^0.2.0" flask-providers-oidc = "^1.2.1" -python-dotenv = "^1.0.0" - -[tool.poetry.dev-dependencies] +flask-pydantic = {git = "https://github.com/MarekPikula/flask-pydantic.git", rev = "dictable_models"} +headscale-api = {git = "https://github.com/MarekPikula/python-headscale-api.git"} +betterproto = {git = "https://github.com/MarekPikula/python-betterproto.git", rev = "classmethod_from_dict"} +apscheduler = "^3.10.1" +tzdata = "^2023.3" [tool.poetry.group.dev.dependencies] pylint = "^2.17.0" -autopep8 = "^2.0.2" +black = "^23.3.0" +isort = "^5.12.0" +ruff = "^0.0.260" +pre-commit = "^3.2.1" +mypy = "^1.1.1" +pydocstyle = "^6.3.0" +pylint-pydantic = "^0.1.8" +types-requests = "^2.28.11.17" +coverage = "^7.2.3" +gitpython = "^3.1.31" [build-system] -requires = ["poetry-core>=1.0.0"] \ No newline at end of file +requires = ["poetry-core>=1.0.0"] + +[tool.isort] +profile = "black" + +[tool.pylint.main] +extension-pkg-whitelist = ["pydantic"] +load-plugins = ["pylint_pydantic"] +generated-members = "app.logger.debug,\napp.logger.info,\napp.logger.warning,\napp.logger.error,\napp.logger.critical,\napp.logger.exception,\napp.logger.setLevel" diff --git a/renderer.py b/renderer.py index d2ea6fa..294be4f 100644 --- a/renderer.py +++ b/renderer.py @@ -1,224 +1,269 @@ -# pylint: disable=line-too-long, wrong-import-order +# pylint: disable=too-many-lines +"""Page rendering functions. -import headscale, helper, pytz, os, yaml, logging, json -from flask import Flask, Markup, render_template -from datetime import datetime -from dateutil import parser -from concurrent.futures import ALL_COMPLETED, wait -from flask_executor import Executor +TODO: Move some parts to Jinja templates. +""" -LOG_LEVEL = os.environ["LOG_LEVEL"].replace('"', '').upper() -# Initiate the Flask application and logging: -app = Flask(__name__, static_url_path="/static") -match LOG_LEVEL: - case "DEBUG" : app.logger.setLevel(logging.DEBUG) - case "INFO" : app.logger.setLevel(logging.INFO) - case "WARNING" : app.logger.setLevel(logging.WARNING) - case "ERROR" : app.logger.setLevel(logging.ERROR) - case "CRITICAL": app.logger.setLevel(logging.CRITICAL) -executor = Executor(app) -def render_overview(): - app.logger.info("Rendering the Overview page") - url = headscale.get_url() - api_key = headscale.get_api_key() +import asyncio +import datetime - timezone = pytz.timezone(os.environ["TZ"] if os.environ["TZ"] else "UTC") - local_time = timezone.localize(datetime.now()) - - # Overview page will just read static information from the config file and display it - # Open the config.yaml and parse it. - config_file = "" - try: - config_file = open("/etc/headscale/config.yml", "r") - app.logger.info("Opening /etc/headscale/config.yml") - except: - config_file = open("/etc/headscale/config.yaml", "r") - app.logger.info("Opening /etc/headscale/config.yaml") - config_yaml = yaml.safe_load(config_file) +from flask import current_app, render_template +from flask_oidc import OpenIDConnect # type: ignore +from headscale_api.schema.headscale import v1 as schema +from markupsafe import Markup - # Get and display the following information: - # Overview of the server's machines, users, preauth keys, API key expiration, server version - - # Get all machines: - machines = headscale.get_machines(url, api_key) - machines_count = len(machines["machines"]) +import helper +from config import Config +from headscale import HeadscaleApi + + +async def render_overview(headscale: HeadscaleApi): # pylint: disable=too-many-locals + """Render the overview page.""" + current_app.logger.info("Rendering the Overview page") + + local_time = datetime.datetime.now(headscale.app_config.timezone) + + # Get and display overview of the following information: + # server's machines, users, preauth keys, API key expiration, server version + + async with headscale.session: + machines, routes, users = await asyncio.gather( + headscale.list_machines(schema.ListMachinesRequest("")), + headscale.get_routes(schema.GetRoutesRequest()), + headscale.list_users(schema.ListUsersRequest()), + ) + user_preauth_keys: list[schema.ListPreAuthKeysResponse] = await asyncio.gather( + *[ + headscale.list_pre_auth_keys(schema.ListPreAuthKeysRequest(user.name)) + for user in users.users + ] + ) # Need to check if routes are attached to an active machine: - # ISSUE: https://github.com/iFargle/headscale-webui/issues/36 - # ISSUE: https://github.com/juanfont/headscale/issues/1228 + # ISSUE: https://github.com/iFargle/headscale-webui/issues/36 + # ISSUE: https://github.com/juanfont/headscale/issues/1228 # Get all routes: - routes = headscale.get_routes(url,api_key) - - total_routes = 0 - for route in routes["routes"]: - if int(route['machine']['id']) != 0: - total_routes += 1 - - enabled_routes = 0 - for route in routes["routes"]: - if route["enabled"] and route['advertised'] and int(route['machine']['id']) != 0: - enabled_routes += 1 + total_routes = sum(route.machine.id != 0 for route in routes.routes) + enabled_routes = sum( + route.enabled and route.advertised and route.machine.id != 0 + for route in routes.routes + ) # Get a count of all enabled exit routes exits_count = 0 exits_enabled_count = 0 - for route in routes["routes"]: - if route['advertised'] and int(route['machine']['id']) != 0: - if route["prefix"] == "0.0.0.0/0" or route["prefix"] == "::/0": - exits_count +=1 - if route["enabled"]: + for route in routes.routes: + if route.advertised and route.machine.id != 0: + if route.prefix in ("0.0.0.0/0", "::/0"): + exits_count += 1 + if route.enabled: exits_enabled_count += 1 # Get User and PreAuth Key counts - user_count = 0 - usable_keys_count = 0 - users = headscale.get_users(url, api_key) - for user in users["users"]: - user_count +=1 - preauth_keys = headscale.get_preauth_keys(url, api_key, user["name"]) - for key in preauth_keys["preAuthKeys"]: - expiration_parse = parser.parse(key["expiration"]) - key_expired = True if expiration_parse < local_time else False - if key["reusable"] and not key_expired: usable_keys_count += 1 - if not key["reusable"] and not key["used"] and not key_expired: usable_keys_count += 1 - - # General Content variables: - ip_prefixes, server_url, disable_check_updates, ephemeral_node_inactivity_timeout, node_update_check_interval = "N/A", "N/A", "N/A", "N/A", "N/A" - if "ip_prefixes" in config_yaml: ip_prefixes = str(config_yaml["ip_prefixes"]) - if "server_url" in config_yaml: server_url = str(config_yaml["server_url"]) - if "disable_check_updates" in config_yaml: disable_check_updates = str(config_yaml["disable_check_updates"]) - if "ephemeral_node_inactivity_timeout" in config_yaml: ephemeral_node_inactivity_timeout = str(config_yaml["ephemeral_node_inactivity_timeout"]) - if "node_update_check_interval" in config_yaml: node_update_check_interval = str(config_yaml["node_update_check_interval"]) - - # OIDC Content variables: - issuer, client_id, scope, use_expiry_from_token, expiry = "N/A", "N/A", "N/A", "N/A", "N/A" - if "oidc" in config_yaml: - if "issuer" in config_yaml["oidc"] : issuer = str(config_yaml["oidc"]["issuer"]) - if "client_id" in config_yaml["oidc"] : client_id = str(config_yaml["oidc"]["client_id"]) - if "scope" in config_yaml["oidc"] : scope = str(config_yaml["oidc"]["scope"]) - if "use_expiry_from_token" in config_yaml["oidc"] : use_expiry_from_token = str(config_yaml["oidc"]["use_expiry_from_token"]) - if "expiry" in config_yaml["oidc"] : expiry = str(config_yaml["oidc"]["expiry"]) - - # Embedded DERP server information. - enabled, region_id, region_code, region_name, stun_listen_addr = "N/A", "N/A", "N/A", "N/A", "N/A" - if "derp" in config_yaml: - if "server" in config_yaml["derp"] and config_yaml["derp"]["server"]["enabled"]: - if "enabled" in config_yaml["derp"]["server"]: enabled = str(config_yaml["derp"]["server"]["enabled"]) - if "region_id" in config_yaml["derp"]["server"]: region_id = str(config_yaml["derp"]["server"]["region_id"]) - if "region_code" in config_yaml["derp"]["server"]: region_code = str(config_yaml["derp"]["server"]["region_code"]) - if "region_name" in config_yaml["derp"]["server"]: region_name = str(config_yaml["derp"]["server"]["region_name"]) - if "stun_listen_addr" in config_yaml["derp"]["server"]: stun_listen_addr = str(config_yaml["derp"]["server"]["stun_listen_addr"]) - - nameservers, magic_dns, domains, base_domain = "N/A", "N/A", "N/A", "N/A" - if "dns_config" in config_yaml: - if "nameservers" in config_yaml["dns_config"]: nameservers = str(config_yaml["dns_config"]["nameservers"]) - if "magic_dns" in config_yaml["dns_config"]: magic_dns = str(config_yaml["dns_config"]["magic_dns"]) - if "domains" in config_yaml["dns_config"]: domains = str(config_yaml["dns_config"]["domains"]) - if "base_domain" in config_yaml["dns_config"]: base_domain = str(config_yaml["dns_config"]["base_domain"]) + usable_keys_count = sum( + sum( + (key.reusable or (not key.reusable and not key.used)) + and not key.expiration < local_time + for key in preauth_keys.pre_auth_keys + ) + for preauth_keys in user_preauth_keys + ) # Start putting the content together - overview_content = """ -
-
-
-
    -
  • Server Statistics

  • -
  • Machines Added
    """+ str(machines_count) +"""
  • -
  • Users Added
    """+ str(user_count) +"""
  • -
  • Usable Preauth Keys
    """+ str(usable_keys_count) +"""
  • -
  • Enabled/Total Routes
    """+ str(enabled_routes) +"""/"""+str(total_routes) +"""
  • -
  • Enabled/Total Exits
    """+ str(exits_enabled_count) +"""/"""+str(exits_count)+"""
  • -
+ overview_content = f""" +
+
+
+
    +
  • Server Statistics

  • +
  • Machines Added +
    + {len(machines.machines)}
  • +
  • Users Added +
    + {len(users.users)}
  • +
  • Usable Preauth Keys +
    + {usable_keys_count}
  • +
  • Enabled/Total Routes +
    + {enabled_routes}/{total_routes}
  • +
  • Enabled/Total Exits +
    + {exits_enabled_count}/{exits_count}
  • +
+
+
-
-
- """ - general_content = """ -
-
-
-
    -
  • General

  • -
  • IP Prefixes
    """+ ip_prefixes +"""
  • -
  • Server URL
    """+ server_url +"""
  • -
  • Updates Disabled
    """+ disable_check_updates +"""
  • -
  • Ephemeral Node Inactivity Timeout
    """+ ephemeral_node_inactivity_timeout +"""
  • -
  • Node Update Check Interval
    """+ node_update_check_interval +"""
  • -
-
-
-
- """ - oidc_content = """ -
-
-
-
    -
  • Headscale OIDC

  • -
  • Issuer
    """+ issuer +"""
  • -
  • Client ID
    """+ client_id +"""
  • -
  • Scope
    """+ scope +"""
  • -
  • Use OIDC Token Expiry
    """+ use_expiry_from_token +"""
  • -
  • Expiry
    """+ expiry +"""
  • -
-
-
-
- """ - derp_content = """ -
-
-
-
    -
  • Embedded DERP

  • -
  • Enabled
    """+ enabled +"""
  • -
  • Region ID
    """+ region_id +"""
  • -
  • Region Code
    """+ region_code +"""
  • -
  • Region Name
    """+ region_name +"""
  • -
  • STUN Address
    """+ stun_listen_addr +"""
  • -
-
-
-
- """ - dns_content = """ -
-
-
-
    -
  • DNS

  • -
  • DNS Nameservers
    """+ nameservers +"""
  • -
  • MagicDNS
    """+ magic_dns +"""
  • -
  • Search Domains
    """+ domains +"""
  • -
  • Base Domain
    """+ base_domain +"""
  • -
-
-
-
- """ + """ - # Remove content that isn't needed: - # Remove OIDC if it isn't available: - if "oidc" not in config_yaml: oidc_content = "" - # Remove DERP if it isn't available or isn't enabled - if "derp" not in config_yaml: derp_content = "" - if "derp" in config_yaml: - if "server" in config_yaml["derp"]: - if str(config_yaml["derp"]["server"]["enabled"]) == "False": - derp_content = "" + # Overview page will just read static information from the config file and display + # it Open the config.yaml and parse it. + config_yaml = headscale.hs_config - # TODO: + if config_yaml is None: + return Markup( + f"""
{overview_content} +
+
+
+
    +
  • General

  • +
  • + Headscale configuration is invalid or unavailable. + Please check logs.
  • +
+
+
+
+ """ + ) + + general_content = f""" +
+
+
+
    +
  • General

  • +
  • IP Prefixes +
    + {config_yaml.ip_prefixes or 'N/A'}
  • +
  • Server URL +
    + {config_yaml.server_url}
  • +
  • Updates Disabled +
    + {config_yaml.disable_check_updates or 'N/A'} +
  • +
  • Ephemeral Node Inactivity Timeout +
    + {config_yaml.ephemeral_node_inactivity_timeout or 'N/A'} +
  • +
  • Node Update Check Interval +
    + {config_yaml.node_update_check_interval or 'N/A'} +
  • +
+
+
+
+ """ + + # OIDC Content: + oidc = config_yaml.oidc + oidc_content = ( + ( + f""" +
+
+
+
    +
  • Headscale OIDC

  • +
  • Issuer +
    + {oidc.issuer or 'N/A'}
  • +
  • Client ID +
    + {oidc.client_id or 'N/A'}
  • +
  • Scope +
    + {oidc.scope or 'N/A'}
  • +
  • Use OIDC Token Expiry +
    + {oidc.use_expiry_from_token or 'N/A'}
  • +
  • Expiry +
    + {oidc.expiry or 'N/A'}
  • +
+
+
+
+ """ + ) + if oidc is not None + else "" + ) + + # Embedded DERP server information. + derp = config_yaml.derp + derp_content = ( + ( + f""" +
+
+
+
    +
  • Embedded DERP

  • +
  • Enabled +
    + {derp.server.enabled}
  • +
  • Region ID +
    + {derp.server.region_id or 'N/A'}
  • +
  • Region Code +
    + {derp.server.region_code or 'N/A'}
  • +
  • Region Name +
    + {derp.server.region_name or 'N/A'}
  • +
  • STUN Address +
    + {derp.server.stun_listen_addr or 'N/A'}
  • +
+
+
+
+ """ + ) + if derp is not None and derp.server is not None and derp.server.enabled + else "" + ) + + dns_config = config_yaml.dns_config + dns_content = ( + ( + f""" +
+
+
+
    +
  • DNS

  • +
  • DNS Nameservers +
    + {dns_config.nameservers or 'N/A'}
  • +
  • MagicDNS +
    + {dns_config.magic_dns or 'N/A'}
  • +
  • Search Domains +
    + {dns_config.domains or 'N/A'}
  • +
  • Base Domain +
    + {dns_config.base_domain or 'N/A'}
  • +
+
+
+
+ """ + ) + if dns_config is not None + else "" + ) + + # TODO: # Whether there are custom DERP servers - # If there are custom DERP servers, get the file location from the config file. Assume mapping is the same. - # Whether the built-in DERP server is enabled + # If there are custom DERP servers, get the file location from the config + # file. Assume mapping is the same. + # Whether the built-in DERP server is enabled # The IP prefixes # The DNS config - if config_yaml["derp"]["paths"]: pass + # if derp is not None and derp.paths is not None: + # pass # # open the path: - # derp_file = + # derp_file = # config_file = open("/etc/headscale/config.yaml", "r") # config_yaml = yaml.safe_load(config_file) # The ACME config, if not empty @@ -227,27 +272,44 @@ def render_overview(): # The log level # What kind of Database is being used to drive headscale - content = "
" + overview_content + general_content + derp_content + oidc_content + dns_content + "" - return Markup(content) + return Markup( + "
" + + overview_content + + general_content + + derp_content + + oidc_content + + dns_content + ) -def thread_machine_content(machine, machine_content, idx, all_routes, failover_pair_prefixes): + +async def thread_machine_content( # pylint: disable=all + headscale: HeadscaleApi, + machine: schema.Machine, + idx: int, + all_routes: schema.GetRoutesResponse, +) -> str: + """Render a single machine.""" # machine = passed in machine information # content = place to write the content - # app.logger.debug("Machine Information") - # app.logger.debug(str(machine)) - app.logger.debug("Machine Information =================") - app.logger.debug("Name: %s, ID: %s, User: %s, givenName: %s, ", str(machine["name"]), str(machine["id"]), str(machine["user"]["name"]), str(machine["givenName"])) - - url = headscale.get_url() - api_key = headscale.get_api_key() + failover_pair_prefixes: list[str] = [] + current_app.logger.debug("Machine Information =================") + current_app.logger.debug( + "Name: %s, ID: %i, User: %s, givenName: %s", + machine.name, + machine.id, + machine.user.name, + machine.given_name, + ) # Set the current timezone and local time - timezone = pytz.timezone(os.environ["TZ"] if os.environ["TZ"] else "UTC") - local_time = timezone.localize(datetime.now()) + timezone = headscale.app_config.timezone + local_time = datetime.datetime.now(timezone) # Get the machines routes - pulled_routes = headscale.get_machine_routes(url, api_key, machine["id"]) + pulled_routes = await headscale.get_machine_routes( + schema.GetMachineRoutesRequest(machine.id) + ) routes = "" # Test if the machine is an exit node: @@ -257,445 +319,594 @@ def thread_machine_content(machine, machine_content, idx, all_routes, failover_p ha_enabled = False # If the length of "routes" is NULL/0, there are no routes, enabled or disabled: - if len(pulled_routes["routes"]) > 0: - advertised_routes = False + if len(pulled_routes.routes) > 0: # pylint: disable=too-many-nested-blocks + # First, check if there are any routes that are both enabled and advertised If + # that is true, we will output the collection-item for routes. Otherwise, it + # will not be displayed. + advertised_routes = any(route.advertised for route in pulled_routes.routes) - # First, check if there are any routes that are both enabled and advertised - # If that is true, we will output the collection-item for routes. Otherwise, it will not be displayed. - for route in pulled_routes["routes"]: - if route["advertised"]: - advertised_routes = True if advertised_routes: routes = """
  • directions Routes

    - """ - # app.logger.debug("Pulled Routes Dump: "+str(pulled_routes)) - # app.logger.debug("All Routes Dump: "+str(all_routes)) + """ + # current_app.logger.debug("Pulled Routes Dump: "+str(pulled_routes)) + # current_app.logger.debug("All Routes Dump: "+str(all_routes)) # Find all exits and put their ID's into the exit_routes array - exit_routes = [] + exit_routes: list[int] = [] exit_enabled_color = "red" exit_tooltip = "enable" exit_route_enabled = False - - for route in pulled_routes["routes"]: - if route["prefix"] == "0.0.0.0/0" or route["prefix"] == "::/0": - exit_routes.append(route["id"]) + + for route in pulled_routes.routes: + if route.prefix in ("0.0.0.0/0", "::/0"): + exit_routes.append(route.id) exit_route_found = True # Test if it is enabled: - if route["enabled"]: + if route.enabled: exit_enabled_color = "green" - exit_tooltip = 'disable' + exit_tooltip = "disable" exit_route_enabled = True - app.logger.debug("Found exit route ID's: "+str(exit_routes)) - app.logger.debug("Exit Route Information: ID: %s | Enabled: %s | exit_route_enabled: %s / Found: %s", str(route["id"]), str(route["enabled"]), str(exit_route_enabled), str(exit_route_found)) + current_app.logger.debug("Found exit route ID's: %s", exit_routes) + current_app.logger.debug( + "Exit Route Information: ID: %i | Enabled: %r | " + "exit_route_enabled: %r / Found: %r", + route.id, + route.enabled, + exit_route_enabled, + exit_route_found, + ) # Print the button for the Exit routes: if exit_route_found: - routes = routes+"""

    - Exit Route -

    - """ + routes += ( + f"

    " + "Exit Route

    " + ) - # Check if the route has another enabled identical route. + # Check if the route has another enabled identical route. # Check all routes from the current machine... - for route in pulled_routes["routes"]: + for route in pulled_routes.routes: # ... against all routes from all machines .... - for route_info in all_routes["routes"]: - app.logger.debug("Comparing routes %s and %s", str(route["prefix"]), str(route_info["prefix"])) - # ... If the route prefixes match and are not exit nodes ... - if str(route_info["prefix"]) == str(route["prefix"]) and (route["prefix"] != "0.0.0.0/0" and route["prefix"] != "::/0"): - # Check if the route ID's match. If they don't ... - app.logger.debug("Found a match: %s and %s", str(route["prefix"]), str(route_info["prefix"])) - if route_info["id"] != route["id"]: - app.logger.debug("Route ID's don't match. They're on different nodes.") + for route_info in all_routes.routes: + current_app.logger.debug( + "Comparing routes %s and %s", route.prefix, route_info.prefix + ) + # ... If the route prefixes match and are not exit nodes ... + if route_info.prefix == route.prefix and ( + route.prefix not in ("0.0.0.0/0", "::/0") + ): + # Check if the route ID's match. If they don't ... + current_app.logger.debug( + "Found a match: %s and %s", route.prefix, route_info.prefix + ) + if route_info.id != route.id: + current_app.logger.debug( + "Route ID's don't match. They're on different nodes." + ) # ... Check if the routes prefix is already in the array... - if route["prefix"] not in failover_pair_prefixes: + if route.prefix not in failover_pair_prefixes: # IF it isn't, add it. - app.logger.info("New HA pair found: %s", str(route["prefix"])) - failover_pair_prefixes.append(str(route["prefix"])) - if route["enabled"] and route_info["enabled"]: + current_app.logger.info( + "New HA pair found: %s", route.prefix + ) + failover_pair_prefixes.append(route.prefix) + if route.enabled and route_info.enabled: # If it is already in the array. . . # Show as HA only if both routes are enabled: - app.logger.debug("Both routes are enabled. Setting as HA [%s] (%s) ", str(machine["name"]), str(route["prefix"])) + current_app.logger.debug( + "Both routes are enabled. Setting as HA [%s] (%s) ", + machine.name, + route.prefix, + ) ha_enabled = True - # If the route is an exit node and already counted as a failover route, it IS a failover route, so display it. - if route["prefix"] != "0.0.0.0/0" and route["prefix"] != "::/0" and route["prefix"] in failover_pair_prefixes: + # If the route is an exit node and already counted as a failover route, + # it IS a failover route, so display it. + if ( + route.prefix not in ("0.0.0.0/0", "::/0") + and route.prefix in failover_pair_prefixes + ): route_enabled = "red" - route_tooltip = 'enable' - color_index = failover_pair_prefixes.index(str(route["prefix"])) + route_tooltip = "enable" + color_index = failover_pair_prefixes.index(route.prefix) route_enabled_color = helper.get_color(color_index, "failover") - if route["enabled"]: - color_index = failover_pair_prefixes.index(str(route["prefix"])) + if route.enabled: + color_index = failover_pair_prefixes.index(route.prefix) route_enabled = helper.get_color(color_index, "failover") - route_tooltip = 'disable' - routes = routes+"""

    - """+route['prefix']+""" -

    - """ - + route_tooltip = "disable" + routes += ( + f"

    " + f"{route.prefix}

    " + ) + # Get the remaining routes: - for route in pulled_routes["routes"]: + for route in pulled_routes.routes: # Get the remaining routes - No exits or failover pairs - if route["prefix"] != "0.0.0.0/0" and route["prefix"] != "::/0" and route["prefix"] not in failover_pair_prefixes: - app.logger.debug("Route: ["+str(route['machine']['name'])+"] id: "+str(route['id'])+" / prefix: "+str(route['prefix'])+" enabled?: "+str(route['enabled'])) + if ( + route.prefix not in ("0.0.0.0/0", "::/0") + and route.prefix not in failover_pair_prefixes + ): + current_app.logger.debug( + "Route: [%s] id: %i / prefix: %s enabled?: %r", + route.machine.name, + route.id, + route.prefix, + route.enabled, + ) route_enabled = "red" - route_tooltip = 'enable' - if route["enabled"]: + route_tooltip = "enable" + if route.enabled: route_enabled = "green" - route_tooltip = 'disable' - routes = routes+"""

    - """+route['prefix']+""" -

    - """ - routes = routes+"

  • " + route_tooltip = "disable" + routes += ( + f"

    {route.prefix}

    " + ) + routes += "

    " # Get machine tags - tag_array = "" - for tag in machine["forcedTags"]: - tag_array = tag_array+"{tag: '"+tag[4:]+"'}, " - tags = """ + tag_array = ", ".join(f"{{tag: '{tag[4:]}'}}" for tag in machine.forced_tags) + tags = f"""
  • - label + label Tags -

    +

  • """ # Get the machine IP's - machine_ips = "
      " - for ip_address in machine["ipAddresses"]: - machine_ips = machine_ips+"
    • "+ip_address+"
    • " - machine_ips = machine_ips+"
    " + machine_ips = ( + "
      " + + "".join(f"
    • {ip_address}
    • " for ip_address in machine.ip_addresses) + + "
    " + ) # Format the dates for easy readability - last_seen_parse = parser.parse(machine["lastSeen"]) - last_seen_local = last_seen_parse.astimezone(timezone) - last_seen_delta = local_time - last_seen_local - last_seen_print = helper.pretty_print_duration(last_seen_delta) - last_seen_time = str(last_seen_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+" "+str(timezone)+" ("+str(last_seen_print)+")" - - last_update_parse = local_time if machine["lastSuccessfulUpdate"] is None else parser.parse(machine["lastSuccessfulUpdate"]) - last_update_local = last_update_parse.astimezone(timezone) - last_update_delta = local_time - last_update_local - last_update_print = helper.pretty_print_duration(last_update_delta) - last_update_time = str(last_update_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+" "+str(timezone)+" ("+str(last_update_print)+")" + last_seen_local = machine.last_seen.astimezone(timezone) + last_seen_delta = local_time - last_seen_local + last_seen_print = helper.pretty_print_duration(last_seen_delta) + last_seen_time = ( + str(last_seen_local.strftime("%A %m/%d/%Y, %H:%M:%S")) + + f" {timezone} ({last_seen_print})" + ) - created_parse = parser.parse(machine["createdAt"]) - created_local = created_parse.astimezone(timezone) - created_delta = local_time - created_local - created_print = helper.pretty_print_duration(created_delta) - created_time = str(created_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+" "+str(timezone)+" ("+str(created_print)+")" + if machine.last_successful_update is not None: + last_update_local = machine.last_successful_update.astimezone(timezone) + last_update_delta = local_time - last_update_local + last_update_print = helper.pretty_print_duration(last_update_delta) + last_update_time = ( + str(last_update_local.strftime("%A %m/%d/%Y, %H:%M:%S")) + + f" {timezone} ({last_update_print})" + ) + else: + last_update_print = None + last_update_time = None + + created_local = machine.created_at.astimezone(timezone) + created_delta = local_time - created_local + created_print = helper.pretty_print_duration(created_delta) + created_time = ( + str(created_local.strftime("%A %m/%d/%Y, %H:%M:%S")) + + f" {timezone} ({created_print})" + ) # If there is no expiration date, we don't need to do any calculations: - if machine["expiry"] != "0001-01-01T00:00:00Z": - expiry_parse = parser.parse(machine["expiry"]) - expiry_local = expiry_parse.astimezone(timezone) - expiry_delta = expiry_local - local_time - expiry_print = helper.pretty_print_duration(expiry_delta, "expiry") - if str(expiry_local.strftime('%Y')) in ("0001", "9999", "0000"): - expiry_time = "No expiration date." - elif int(expiry_local.strftime('%Y')) > int(expiry_local.strftime('%Y'))+2: - expiry_time = str(expiry_local.strftime('%m/%Y'))+" "+str(timezone)+" ("+str(expiry_print)+")" - else: - expiry_time = str(expiry_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+" "+str(timezone)+" ("+str(expiry_print)+")" + if machine.expiry != datetime.datetime(1, 1, 1, 0, 0, tzinfo=datetime.timezone.utc): + expiry_local = machine.expiry.astimezone(timezone) + expiry_delta = expiry_local - local_time + expiry_print = helper.pretty_print_duration(expiry_delta, "expiry") + if str(expiry_local.strftime("%Y")) in ("0001", "9999", "0000"): + expiry_time = "No expiration date." + elif int(expiry_local.strftime("%Y")) > int(expiry_local.strftime("%Y")) + 2: + expiry_time = ( + str(expiry_local.strftime("%m/%Y")) + f" {timezone} ({expiry_print})" + ) + else: + expiry_time = ( + str(expiry_local.strftime("%A %m/%d/%Y, %H:%M:%S")) + + f" {timezone} ({expiry_print})" + ) - expiring_soon = True if int(expiry_delta.days) < 14 and int(expiry_delta.days) > 0 else False - app.logger.debug("Machine: "+machine["name"]+" expires: "+str(expiry_local.strftime('%Y'))+" / "+str(expiry_delta.days)) + expiring_soon = int(expiry_delta.days) < 14 and int(expiry_delta.days) > 0 + current_app.logger.debug( + "Machine: %s expires: %s / %i", + machine.name, + expiry_local.strftime("%Y"), + expiry_delta.days, + ) else: - expiry_time = "No expiration date." + expiry_time = "No expiration date." expiring_soon = False - app.logger.debug("Machine: "+machine["name"]+" has no expiration date") - + current_app.logger.debug("Machine: %s has no expiration date", machine.name) # Get the first 10 characters of the PreAuth Key: - if machine["preAuthKey"]: - preauth_key = str(machine["preAuthKey"]["key"])[0:10] - else: preauth_key = "None" + if machine.pre_auth_key is not None: + preauth_key = machine.pre_auth_key.key[0:10] + else: + preauth_key = "None" # Set the status and user badge color: text_color = helper.text_color_duration(last_seen_delta) - user_color = helper.get_color(int(machine["user"]["id"])) + user_color = helper.get_color(int(machine.user.id)) # Generate the various badges: - status_badge = "fiber_manual_record" - user_badge = ""+machine["user"]["name"]+"" - exit_node_badge = "" if not exit_route_enabled else "Exit" - ha_route_badge = "" if not ha_enabled else "HA" - expiration_badge = "" if not expiring_soon else "Expiring!" + status_badge = ( + f"" + "fiber_manual_record" + ) + user_badge = ( + f"{machine.user.name}" + ) + exit_node_badge = ( + "" + if not exit_route_enabled + else ( + "Exit" + ) + ) + ha_route_badge = ( + "" + if not ha_enabled + else ( + "HA" + ) + ) + expiration_badge = ( + "" + if not expiring_soon + else ( + "" + "Expiring!" + ) + ) - machine_content[idx] = (str(render_template( - 'machines_card.html', - given_name = machine["givenName"], - machine_id = machine["id"], - hostname = machine["name"], - ns_name = machine["user"]["name"], - ns_id = machine["user"]["id"], - ns_created = machine["user"]["createdAt"], - last_seen = str(last_seen_print), - last_update = str(last_update_print), - machine_ips = Markup(machine_ips), - advertised_routes = Markup(routes), - exit_node_badge = Markup(exit_node_badge), - ha_route_badge = Markup(ha_route_badge), - status_badge = Markup(status_badge), - user_badge = Markup(user_badge), - last_update_time = str(last_update_time), - last_seen_time = str(last_seen_time), - created_time = str(created_time), - expiry_time = str(expiry_time), - preauth_key = str(preauth_key), - expiration_badge = Markup(expiration_badge), - machine_tags = Markup(tags), - taglist = machine["forcedTags"] - ))) - app.logger.info("Finished thread for machine "+machine["givenName"]+" index "+str(idx)) + current_app.logger.info( + "Finished thread for machine %s index %i", machine.given_name, idx + ) + return render_template( + "machines_card.html", + given_name=machine.given_name, + machine_id=machine.id, + hostname=machine.name, + ns_name=machine.user.name, + ns_id=machine.user.id, + ns_created=machine.user.created_at, + last_seen=str(last_seen_print), + last_update=str(last_update_print), + machine_ips=Markup(machine_ips), + advertised_routes=Markup(routes), + exit_node_badge=Markup(exit_node_badge), + ha_route_badge=Markup(ha_route_badge), + status_badge=Markup(status_badge), + user_badge=Markup(user_badge), + last_update_time=str(last_update_time), + last_seen_time=str(last_seen_time), + created_time=str(created_time), + expiry_time=str(expiry_time), + preauth_key=str(preauth_key), + expiration_badge=Markup(expiration_badge), + machine_tags=Markup(tags), + taglist=machine.forced_tags, + ) -def render_machines_cards(): - app.logger.info("Rendering machine cards") - url = headscale.get_url() - api_key = headscale.get_api_key() - machines_list = headscale.get_machines(url, api_key) - ######################################### - # Thread this entire thing. - num_threads = len(machines_list["machines"]) - iterable = [] - machine_content = {} - failover_pair_prefixes = [] - for i in range (0, num_threads): - app.logger.debug("Appending iterable: "+str(i)) - iterable.append(i) - # Flask-Executor Method: +async def render_machines_cards(headscale: HeadscaleApi): + """Render machine cards.""" + current_app.logger.info("Rendering machine cards") - # Get all routes - all_routes = headscale.get_routes(url, api_key) - # app.logger.debug("All found routes") - # app.logger.debug(str(all_routes)) + async with headscale.session: + # Execute concurrent machine info requests and sort them by machine_id. + routes = await headscale.get_routes(schema.GetRoutesRequest()) + content = await asyncio.gather( + *[ + thread_machine_content(headscale, machine, idx, routes) + for idx, machine in enumerate( + ( + await headscale.list_machines(schema.ListMachinesRequest("")) + ).machines + ) + ] + ) + return Markup("") - if LOG_LEVEL == "DEBUG": - # DEBUG: Do in a forloop: - for idx in iterable: thread_machine_content(machines_list["machines"][idx], machine_content, idx, all_routes, failover_pair_prefixes) - else: - app.logger.info("Starting futures") - futures = [executor.submit(thread_machine_content, machines_list["machines"][idx], machine_content, idx, all_routes, failover_pair_prefixes) for idx in iterable] - # Wait for the executor to finish all jobs: - wait(futures, return_when=ALL_COMPLETED) - app.logger.info("Finished futures") - # Sort the content by machine_id: - sorted_machines = {key: val for key, val in sorted(machine_content.items(), key = lambda ele: ele[0])} +async def render_users_cards(headscale: HeadscaleApi): + """Render users cards.""" + current_app.logger.info("Rendering Users cards") - content = "" - return Markup(content) +async def build_user_card(headscale: HeadscaleApi, user: schema.User): + """Build a user card.""" + # Get all preAuth Keys in the user, only display if one exists: + preauth_keys_collection = await build_preauth_key_table( + headscale, schema.ListPreAuthKeysRequest(user.name) + ) -def render_users_cards(): - app.logger.info("Rendering Users cards") - url = headscale.get_url() - api_key = headscale.get_api_key() - user_list = headscale.get_users(url, api_key) + # Set the user badge color: + user_color = helper.get_color(int(user.id), "text") - content = "" - return Markup(content) +async def build_preauth_key_table( + headscale: HeadscaleApi, request: schema.ListPreAuthKeysRequest +): # pylint: disable=too-many-locals + """Build PreAuth key table for a user.""" + current_app.logger.info( + "Building the PreAuth key table for User: %s", request.user + ) -def build_preauth_key_table(user_name): - app.logger.info("Building the PreAuth key table for User: %s", str(user_name)) - url = headscale.get_url() - api_key = headscale.get_api_key() - - preauth_keys = headscale.get_preauth_keys(url, api_key, user_name) - preauth_keys_collection = """
  • + preauth_keys = await headscale.list_pre_auth_keys(request) + preauth_keys_collection = f""" +
  • Toggle Expired - Add PreAuth Key vpn_key PreAuth Keys + """ + if len(preauth_keys.pre_auth_keys) == 0: + preauth_keys_collection += "

    No keys defined for this user

    " + else: + preauth_keys_collection += f""" + + + + + + + + + + """ - if len(preauth_keys["preAuthKeys"]) == 0: preauth_keys_collection += "

    No keys defined for this user

    " - if len(preauth_keys["preAuthKeys"]) > 0: - preauth_keys_collection += """ -
    IDKey Prefix
    Reusable
    Used
    Ephemeral
    Usable
    Actions
    - - - - - - - - - - - - """ - for key in preauth_keys["preAuthKeys"]: + for key in preauth_keys.pre_auth_keys: # Get the key expiration date and compare it to now to check if it's expired: # Set the current timezone and local time - timezone = pytz.timezone(os.environ["TZ"] if os.environ["TZ"] else "UTC") - local_time = timezone.localize(datetime.now()) - expiration_parse = parser.parse(key["expiration"]) - key_expired = True if expiration_parse < local_time else False - expiration_time = str(expiration_parse.strftime('%A %m/%d/%Y, %H:%M:%S'))+" "+str(timezone) + timezone = headscale.app_config.timezone + local_time = datetime.datetime.now(timezone) + key_expired = key.expiration < local_time + expiration_time = ( + key.expiration.strftime("%A %m/%d/%Y, %H:%M:%S") + f" {timezone}" + ) + + key_usable = (key.reusable and not key_expired) or ( + not key.reusable and not key.used and not key_expired + ) - key_usable = False - if key["reusable"] and not key_expired: key_usable = True - if not key["reusable"] and not key["used"] and not key_expired: key_usable = True - # Class for the javascript function to look for to toggle the hide function hide_expired = "expired-row" if not key_usable else "" - btn_reusable = "fiber_manual_record" if key["reusable"] else "" - btn_ephemeral = "fiber_manual_record" if key["ephemeral"] else "" - btn_used = "fiber_manual_record" if key["used"] else "" - btn_usable = "fiber_manual_record" if key_usable else "" + btn_reusable = ( + "" + "::" + "fiber_manual_record" + if key.reusable + else "" + ) + btn_ephemeral = ( + "" + "fiber_manual_record" + if key.ephemeral + else "" + ) + btn_used = ( + "" + "fiber_manual_record" + if key.used + else "" + ) + btn_usable = ( + "" + "fiber_manual_record" + if key_usable + else "" + ) # Other buttons: - btn_delete = "Expire" if key_usable else "" - tooltip_data = "Expiration: "+expiration_time + btn_delete = ( + "' + "Expire" + if key_usable + else "" + ) + tooltip_data = f"Expiration: {expiration_time}" # TR ID will look like "1-albert-tr" - preauth_keys_collection = preauth_keys_collection+""" - - - - - - - - + preauth_keys_collection += f""" + + + + + + + + - """ + """ - preauth_keys_collection = preauth_keys_collection+"""
    IDKey Prefix
    Reusable
    Used
    Ephemeral
    Usable
    Actions
    """+str(key["id"])+""""""+str(key["key"])[0:10]+"""
    """+btn_reusable+"""
    """+btn_used+"""
    """+btn_ephemeral+"""
    """+btn_usable+"""
    """+btn_delete+"""
    {key.id}{key.key[0:10]}
    {btn_reusable}
    {btn_used}
    {btn_ephemeral}
    {btn_usable}
    {btn_delete}
    -
  • - """ - return preauth_keys_collection + return preauth_keys_collection + "" -def oidc_nav_dropdown(user_name, email_address, name): - app.logger.info("OIDC is enabled. Building the OIDC nav dropdown") - html_payload = """ + +def oidc_nav_dropdown(user_name: str, email_address: str, name: str) -> Markup: + """Render desktop navigation for OIDC.""" + current_app.logger.debug("OIDC is enabled. Building the OIDC nav dropdown") + html_payload = f"""
  • - """+name+""" account_circle + {name} account_circle
  • - """ + """ return Markup(html_payload) -def oidc_nav_mobile(user_name, email_address, name): - html_payload = """ -

  • exit_to_appLogout
  • + +def oidc_nav_mobile(): + """Render mobile navigation for OIDC.""" + return Markup( + '

  • ' + "exit_to_appLogout
  • " + ) + + +def render_defaults( + config: Config, oidc_handler: OpenIDConnect | None +) -> dict[str, Markup | str]: + """Render the default elements. + + TODO: Think about caching the results. """ - return Markup(html_payload) + colors = { + "color_nav": config.color_nav, + "color_btn": config.color_btn, + } + + if oidc_handler is None: + return colors + + # If OIDC is enabled, display the buttons: + email_address: str = oidc_handler.user_getfield("email") # type: ignore + assert isinstance(email_address, str) + user_name: str = oidc_handler.user_getfield("preferred_username") # type: ignore + assert isinstance(user_name, str) + name: str = oidc_handler.user_getfield("name") # type: ignore + assert isinstance(name, str) + + return { + "oidc_nav_dropdown": oidc_nav_dropdown(user_name, email_address, name), + "oidc_nav_mobile": oidc_nav_mobile(), + **colors, + } + def render_search(): - html_payload = """ -
  • - search -
  • - """ - return Markup(html_payload) + """Render search bar.""" + return Markup( + """ +
  • + search +
  • + """ + ) -def render_routes(): - app.logger.info("Rendering Routes page") - url = headscale.get_url() - api_key = headscale.get_api_key() - all_routes = headscale.get_routes(url, api_key) + +async def render_routes( + headscale: HeadscaleApi, +): # pylint: disable=too-many-branches,too-many-statements,too-many-locals + """Render routes page.""" + current_app.logger.info("Rendering Routes page") + all_routes = await headscale.get_routes(schema.GetRoutesRequest()) # If there are no routes, just exit: - if len(all_routes) == 0: return Markup("


    There are no routes to display!
    ") + if len(all_routes.routes) == 0: + return Markup("


    There are no routes to display!
    ") # Get a list of all Route ID's to iterate through: - all_routes_id_list = [] - for route in all_routes["routes"]: - all_routes_id_list.append(route["id"]) - if route["machine"]["name"]: - app.logger.info("Found route %s / machine: %s", str(route["id"]), route["machine"]["name"]) - else: - app.logger.info("Route id %s has no machine associated.", str(route["id"])) + all_routes_id_list: list[int] = [] + for route in all_routes.routes: + all_routes_id_list.append(route.id) + if route.machine.name: + current_app.logger.info( + "Found route %i / machine: %s", route.id, route.machine.name + ) + else: + current_app.logger.info("Route id %i has no machine associated.", route.id) - - route_content = "" + route_content = "" failover_content = "" - exit_content = "" + exit_content = "" - route_title='Routes' - failover_title='Failover Routes' - exit_title='Exit Routes' + route_title = 'Routes' + failover_title = 'Failover Routes' + exit_title = 'Exit Routes' markup_pre = """
    @@ -704,7 +915,7 @@ def render_routes():
    """ - markup_post = """ + markup_post = """
    @@ -714,146 +925,193 @@ def render_routes(): ############################################################################################## # Step 1: Get all non-exit and non-failover routes: - route_content = markup_pre+route_title - route_content += """

    - - - - - - - - - - """ - for route in all_routes["routes"]: + route_content = ( + markup_pre + + route_title + + """ +

    ID Machine Route Enabled
    + + + + + + + + + + """ + ) + for route in all_routes.routes: # Get relevant info: - route_id = route["id"] - machine = route["machine"]["givenName"] - prefix = route["prefix"] - is_enabled = route["enabled"] - is_primary = route["isPrimary"] + machine = route.machine.given_name + prefix = route.prefix + is_enabled = route.enabled + is_primary = route.is_primary is_failover = False - is_exit = False + is_exit = False - enabled = "fiber_manual_record" - disabled = "fiber_manual_record" + enabled = ( + f"fiber_manual_record" + ) + disabled = ( + f"fiber_manual_record" + ) # Set the displays: - enabled_display = disabled + enabled_display = disabled - if is_enabled: enabled_display = enabled + if is_enabled: + enabled_display = enabled # Check if a prefix is an Exit route: - if prefix == "0.0.0.0/0" or prefix == "::/0": is_exit = True + if prefix in ("0.0.0.0/0", "::/0"): + is_exit = True # Check if a prefix is part of a failover pair: - for route_check in all_routes["routes"]: - if not is_exit: - if route["prefix"] == route_check["prefix"]: - if route["id"] != route_check["id"]: - is_failover = True + for route_check in all_routes.routes: + if ( + not is_exit + and route.prefix == route_check.prefix + and route.id != route_check.id + ): + is_failover = True if not is_exit and not is_failover and machine != "": - # Build a simple table for all non-exit routes: - route_content += """ - - - - - - - """ - route_content += "
    ID Machine Route Enabled
    """+str(route_id )+""""""+str(machine )+""""""+str(prefix )+"""
    """+str(enabled_display )+"""

    "+markup_post + # Build a simple table for all non-exit routes: + route_content += f""" + {route.id} + {machine} + {prefix} +
    {enabled_display}
    + """ + route_content += "

    " + markup_post ############################################################################################## # Step 2: Get all failover routes only. Add a separate table per failover prefix - failover_route_prefix = [] - failover_available = False - for route in all_routes["routes"]: - # Get a list of all prefixes for all routes... - for route_check in all_routes["routes"]: - # ... that aren't exit routes... - if route["prefix"] !="0.0.0.0/0" and route["prefix"] != "::/0": - # if the curren route matches any prefix of any other route... - if route["prefix"] == route_check["prefix"]: - # and the route ID's are different ... - if route["id"] != route_check["id"]: - # ... and the prefix is not already in the list... - if route["prefix"] not in failover_route_prefix: - # append the prefix to the failover_route_prefix list - failover_route_prefix.append(route["prefix"]) - failover_available = True + # Get a set of all prefixes for all routes: + # - that aren't exit routes + # - the current route matches any prefix of any other route + # - the route ID's are different + failover_route_prefix = set( + route.prefix + for route_check in all_routes.routes + for route in all_routes.routes + if ( + route.prefix not in ("0.0.0.0/0", "::/0") + and route.prefix == route.prefix + and route.id != route_check.id + ) + ) - if failover_available: + if len(failover_route_prefix) > 0: # Set up the display code: - enabled = "fiber_manual_record" - disabled = "fiber_manual_record" + enabled = ( + "" + "fiber_manual_record" + ) + disabled = ( + "fiber_manual_record" + ) - failover_content = markup_pre+failover_title + failover_content = markup_pre + failover_title # Build the display for failover routes: for route_prefix in failover_route_prefix: # Get all route ID's associated with the route_prefix: - route_id_list = [] - for route in all_routes["routes"]: - if route["prefix"] == route_prefix: - route_id_list.append(route["id"]) + route_id_list = [ + route.id for route in all_routes.routes if route.prefix == route_prefix + ] # Set up the display code: - failover_enabled = "fiber_manual_record" - failover_disabled = "fiber_manual_record" + failover_enabled = ( + f"fiber_manual_record" + ) + failover_disabled = ( + f"fiber_manual_record" + ) failover_display = failover_disabled for route_id in route_id_list: # Get the routes index: current_route_index = all_routes_id_list.index(route_id) - if all_routes["routes"][current_route_index]["enabled"]: failover_display = failover_enabled - + if all_routes.routes[current_route_index].enabled: + failover_display = failover_enabled # Get all route_id's associated with the route prefix: - failover_content += """

    -

    """+failover_display+"""
    """+str(route_prefix)+"""
    - - - - - - - - - - """ + failover_content += f"""

    +

    {failover_display}
    {route_prefix}
    +
    MachineEnabledPrimary
    + + + + + + + + + """ # Build the display: for route_id in route_id_list: idx = all_routes_id_list.index(route_id) - machine = all_routes["routes"][idx]["machine"]["givenName"] - machine_id = all_routes["routes"][idx]["machine"]["id"] - is_primary = all_routes["routes"][idx]["isPrimary"] - is_enabled = all_routes["routes"][idx]["enabled"] + machine = all_routes.routes[idx].machine.given_name + machine_id = all_routes.routes[idx].machine.id + is_primary = all_routes.routes[idx].is_primary + is_enabled = all_routes.routes[idx].enabled - payload = [] - for item in route_id_list: payload.append(int(item)) - - app.logger.debug("[%s] Machine: [%s] %s : %s / %s", str(route_id), str(machine_id), str(machine), str(is_enabled), str(is_primary)) - app.logger.debug(str(all_routes["routes"][idx])) + payload = route_id_list.copy() + + current_app.logger.debug( + "[%i] Machine: [%i] %s : %r / %r", + route_id, + machine_id, + machine, + is_enabled, + is_primary, + ) + current_app.logger.debug(str(all_routes.routes[idx])) # Set up the display code: - enabled_display_enabled = "fiber_manual_record" - enabled_display_disabled = "fiber_manual_record" - primary_display_enabled = "fiber_manual_record" - primary_display_disabled = "fiber_manual_record" - + enabled_display_enabled = ( + f"fiber_manual_record" + ) + enabled_display_disabled = ( + f"fiber_manual_record" + ) + primary_display_enabled = ( + f"fiber_manual_record" + ) + primary_display_disabled = ( + f"fiber_manual_record" + ) + # Set displays: - enabled_display = enabled_display_enabled if is_enabled else enabled_display_disabled - primary_display = primary_display_enabled if is_primary else primary_display_disabled + enabled_display = ( + enabled_display_enabled if is_enabled else enabled_display_disabled + ) + primary_display = ( + primary_display_enabled if is_primary else primary_display_disabled + ) # Build a simple table for all non-exit routes: - failover_content += """ + failover_content += f""" - - - + + + """ failover_content += "
    MachineEnabledPrimary
    """+str(machine)+"""
    """+str(enabled_display)+"""
    """+str(primary_display)+"""
    {machine}
    {enabled_display}
    {primary_display}

    " @@ -861,55 +1119,72 @@ def render_routes(): ############################################################################################## # Step 3: Get exit nodes only: - exit_node_list = [] - # Get a list of nodes with exit routes: - for route in all_routes["routes"]: - # For every exit route found, store the machine name in an array: - if route["prefix"] == "0.0.0.0/0" or route["prefix"] == "::/0": - if route["machine"]["givenName"] not in exit_node_list: - exit_node_list.append(route["machine"]["givenName"]) + # Get a set of nodes with exit routes: + exit_node_list = set( + route.machine.given_name + for route in all_routes.routes + if route.prefix in ("0.0.0.0/0", "::/0") + ) # Exit node display building: # Display by machine, not by route - exit_content = markup_pre+exit_title - exit_content += """

    - - - - - - - - """ - # Get exit route ID's for each node in the list: + exit_content = ( + markup_pre + + exit_title + + """ +

    MachineEnabled
    + + + + + + + + """ + ) + # Get exit route ID's for each node in the list: for node in exit_node_list: - node_exit_route_ids = [] + node_exit_route_ids: list[int] = [] exit_enabled = False exit_available = False machine_id = 0 - for route in all_routes["routes"]: - if route["prefix"] == "0.0.0.0/0" or route["prefix"] == "::/0": - if route["machine"]["givenName"] == node: - node_exit_route_ids.append(route["id"]) - machine_id = route["machine"]["id"] - exit_available = True - if route["enabled"]: - exit_enabled = True + for route in all_routes.routes: + if ( + route.prefix in ("0.0.0.0/0", "::/0") + and route.machine.given_name == node + ): + node_exit_route_ids.append(route.id) + machine_id = route.machine.id + exit_available = True + if route.enabled: + exit_enabled = True if exit_available: # Set up the display code: - enabled = "fiber_manual_record" - disabled = "fiber_manual_record" + enabled = ( + f"fiber_manual_record" + ) + disabled = ( + f"fiber_manual_record" + ) # Set the displays: enabled_display = enabled if exit_enabled else disabled - exit_content += """ - - - - - """ - exit_content += "
    MachineEnabled
    """+str(node)+"""
    """+str(enabled_display)+"""

    "+markup_post + exit_content += f""" + + {node} +
    {enabled_display}
    + + """ + exit_content += "

    " + markup_post content = route_content + failover_content + exit_content - return Markup(content) \ No newline at end of file + return Markup(content) diff --git a/server.py b/server.py index 85c0b79..3aab318 100644 --- a/server.py +++ b/server.py @@ -1,532 +1,399 @@ -# pylint: disable=wrong-import-order +"""Headscale WebUI Flask server.""" -import headscale, helper, json, os, pytz, renderer, secrets, requests, logging -from functools import wraps -from datetime import datetime -from flask import Flask, escape, Markup, redirect, render_template, request, url_for -from dateutil import parser -from flask_executor import Executor +import asyncio +import atexit +import datetime +import functools +from multiprocessing import Lock +from typing import Awaitable, Callable, Type, TypeVar + +import headscale_api.schema.headscale.v1 as schema +from aiohttp import ClientConnectionError +from apscheduler.schedulers.background import BackgroundScheduler # type: ignore +from betterproto import Message +from flask import Flask, redirect, render_template, url_for +from flask_pydantic.core import validate +from headscale_api.headscale import UnauthorizedError +from markupsafe import Markup +from pydantic import BaseModel, Field from werkzeug.middleware.proxy_fix import ProxyFix -# Global vars -# Colors: https://materializecss.com/color.html -COLOR = os.environ["COLOR"].replace('"', '').lower() -COLOR_NAV = COLOR+" darken-1" -COLOR_BTN = COLOR+" darken-3" -AUTH_TYPE = os.environ["AUTH_TYPE"].replace('"', '').lower() -LOG_LEVEL = os.environ["LOG_LEVEL"].replace('"', '').upper() -# If LOG_LEVEL is DEBUG, enable Flask debugging: -DEBUG_STATE = True if LOG_LEVEL == "DEBUG" else False +import renderer +from auth import AuthManager +from config import Config, InitCheckError +from headscale import HeadscaleApi -# Initiate the Flask application and logging: -app = Flask(__name__, static_url_path="/static") -match LOG_LEVEL: - case "DEBUG" : app.logger.setLevel(logging.DEBUG) - case "INFO" : app.logger.setLevel(logging.INFO) - case "WARNING" : app.logger.setLevel(logging.WARNING) - case "ERROR" : app.logger.setLevel(logging.ERROR) - case "CRITICAL": app.logger.setLevel(logging.CRITICAL) -executor = Executor(app) -app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) -app.logger.info("Headscale-WebUI Version: "+os.environ["APP_VERSION"]+" / "+os.environ["GIT_BRANCH"]) -app.logger.info("LOG LEVEL SET TO %s", str(LOG_LEVEL)) -app.logger.info("DEBUG STATE: %s", str(DEBUG_STATE)) - -######################################################################################## -# Set Authentication type. Currently "OIDC" and "BASIC" -######################################################################################## -if AUTH_TYPE == "oidc": - # Currently using: flask-providers-oidc - https://pypi.org/project/flask-providers-oidc/ - # - # https://gist.github.com/thomasdarimont/145dc9aa857b831ff2eff221b79d179a/ - # https://www.authelia.com/integration/openid-connect/introduction/ - # https://github.com/steinarvk/flask_oidc_demo - app.logger.info("Loading OIDC libraries and configuring app...") - - DOMAIN_NAME = os.environ["DOMAIN_NAME"] - BASE_PATH = os.environ["SCRIPT_NAME"] if os.environ["SCRIPT_NAME"] != "/" else "" - OIDC_SECRET = os.environ["OIDC_CLIENT_SECRET"] - OIDC_CLIENT_ID = os.environ["OIDC_CLIENT_ID"] - OIDC_AUTH_URL = os.environ["OIDC_AUTH_URL"] - - # Construct client_secrets.json: - response = requests.get(str(OIDC_AUTH_URL)) - oidc_info = response.json() - app.logger.debug("JSON Dumps for OIDC_INFO: "+json.dumps(oidc_info)) - - client_secrets = json.dumps( - { - "web": { - "issuer": oidc_info["issuer"], - "auth_uri": oidc_info["authorization_endpoint"], - "client_id": OIDC_CLIENT_ID, - "client_secret": OIDC_SECRET, - "redirect_uris": [DOMAIN_NAME + BASE_PATH + "/oidc_callback"], - "userinfo_uri": oidc_info["userinfo_endpoint"], - "token_uri": oidc_info["token_endpoint"], - } - } +def create_tainted_app(app: Flask, error: InitCheckError) -> Flask: + """Run tainted version of the Headscale WebUI after encountering an error.""" + app.logger.error( + "Encountered error when trying to run initialization checks. Running in " + "tainted mode (only the error page is available). Correct all errors and " + "restart the server." ) - with open("/app/instance/secrets.json", "w+") as secrets_json: - secrets_json.write(client_secrets) - app.logger.debug("Client Secrets: ") - with open("/app/instance/secrets.json", "r+") as secrets_json: - app.logger.debug("/app/instances/secrets.json:") - app.logger.debug(secrets_json.read()) - - app.config.update({ - 'SECRET_KEY': secrets.token_urlsafe(32), - 'TESTING': DEBUG_STATE, - 'DEBUG': DEBUG_STATE, - 'OIDC_CLIENT_SECRETS': '/app/instance/secrets.json', - 'OIDC_ID_TOKEN_COOKIE_SECURE': True, - 'OIDC_REQUIRE_VERIFIED_EMAIL': False, - 'OIDC_USER_INFO_ENABLED': True, - 'OIDC_OPENID_REALM': 'Headscale-WebUI', - 'OIDC_SCOPES': ['openid', 'profile', 'email'], - 'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post' - }) - from flask_oidc import OpenIDConnect - oidc = OpenIDConnect(app) + @app.route("/") + def catchall_redirect(path: str): # pylint: disable=unused-argument + return redirect(url_for("error_page")) -elif AUTH_TYPE == "basic": - # https://flask-basicauth.readthedocs.io/en/latest/ - app.logger.info("Loading basic auth libraries and configuring app...") - from flask_basicauth import BasicAuth + @app.route("/error") + async def error_page(): + return render_template( + "error.html", + error_message=Markup( + "".join(sub_error.format_message() for sub_error in error) + ), + ) - app.config['BASIC_AUTH_USERNAME'] = os.environ["BASIC_AUTH_USER"].replace('"', '') - app.config['BASIC_AUTH_PASSWORD'] = os.environ["BASIC_AUTH_PASS"] - app.config['BASIC_AUTH_FORCE'] = True + return app - basic_auth = BasicAuth(app) - ######################################################################################## - # Set Authentication type - Dynamically load function decorators - # https://stackoverflow.com/questions/17256602/assertionerror-view-function-mapping-is-overwriting-an-existing-endpoint-functi - ######################################################################################## - # Make a fake decorator for oidc.require_login - # If anyone knows a better way of doing this, please let me know. - class OpenIDConnect(): - def require_login(self, view_func): - @wraps(view_func) - def decorated(*args, **kwargs): - return view_func(*args, **kwargs) - return decorated - oidc = OpenIDConnect() -else: - ######################################################################################## - # Set Authentication type - Dynamically load function decorators - # https://stackoverflow.com/questions/17256602/assertionerror-view-function-mapping-is-overwriting-an-existing-endpoint-functi - ######################################################################################## - # Make a fake decorator for oidc.require_login - # If anyone knows a better way of doing this, please let me know. - class OpenIDConnect(): - def require_login(self, view_func): - @wraps(view_func) - def decorated(*args, **kwargs): - return view_func(*args, **kwargs) - return decorated - oidc = OpenIDConnect() +async def create_app() -> Flask: + """Run Headscale WebUI Flask application. -######################################################################################## -# / pages - User-facing pages -######################################################################################## -@app.route('/') -@app.route('/overview') -@oidc.require_login -def overview_page(): - # Some basic sanity checks: - pass_checks = str(helper.load_checks()) - if pass_checks != "Pass": return redirect(url_for(pass_checks)) + For arguments refer to `Flask.run()` function. + """ + app = Flask(__name__, static_url_path="/static") + app.wsgi_app = ProxyFix( # type: ignore[method-assign] + app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 # type: ignore + ) + try: + # Try to initialize configuration from environment. + config = Config() # type: ignore - # Check if OIDC is enabled. If it is, display the buttons: - OIDC_NAV_DROPDOWN = Markup("") - OIDC_NAV_MOBILE = Markup("") - if AUTH_TYPE == "oidc": - email_address = oidc.user_getfield("email") - user_name = oidc.user_getfield("preferred_username") - name = oidc.user_getfield("name") - OIDC_NAV_DROPDOWN = renderer.oidc_nav_dropdown(user_name, email_address, name) - OIDC_NAV_MOBILE = renderer.oidc_nav_mobile(user_name, email_address, name) + with app.app_context(): + # Try to create authentication handler (including loading auth config). + auth = AuthManager(config) - return render_template('overview.html', - render_page = renderer.render_overview(), - COLOR_NAV = COLOR_NAV, - COLOR_BTN = COLOR_BTN, - OIDC_NAV_DROPDOWN = OIDC_NAV_DROPDOWN, - OIDC_NAV_MOBILE = OIDC_NAV_MOBILE + # Try to create Headscale API interface. + headscale = HeadscaleApi(config) + + # Check health of Headscale API. + if not await headscale.health_check(): + raise ClientConnectionError(f"Health check failed on {headscale.base_url}") + except Exception as error: # pylint: disable=broad-exception-caught + # We want to catch broad exception to ensure no errors whatsoever went through + # the environment init. + with app.app_context(): + check_error = InitCheckError.from_exception(error) + return create_tainted_app(app, check_error) + + app.logger.setLevel(config.log_level) + app.logger.info( + "Headscale-WebUI Version: %s / %s", config.app_version, config.git_branch + ) + app.logger.info("Logger level set to %s.", config.log_level) + app.logger.info("Debug state: %s", config.debug_mode) + + register_pages(app, headscale, auth) + register_api_endpoints(app, headscale, auth) + register_scheduler(app, headscale) + + return app + + +def register_pages(app: Flask, headscale: HeadscaleApi, auth: AuthManager): + """Register user-facing pages.""" + config = headscale.app_config + + # Convenience short for render_defaults + render_defaults = functools.partial( + renderer.render_defaults, config, auth.oidc_handler ) -@app.route('/routes', methods=('GET', 'POST')) -@oidc.require_login -def routes_page(): - # Some basic sanity checks: - pass_checks = str(helper.load_checks()) - if pass_checks != "Pass": return redirect(url_for(pass_checks)) + @app.route("/") + @app.route("/overview") + @auth.require_login + @headscale.key_check_guard + async def overview_page(): + return render_template( + "overview.html", + render_page=await renderer.render_overview(headscale), + **render_defaults(), + ) - # Check if OIDC is enabled. If it is, display the buttons: - OIDC_NAV_DROPDOWN = Markup("") - OIDC_NAV_MOBILE = Markup("") - INPAGE_SEARCH = Markup(renderer.render_search()) - if AUTH_TYPE == "oidc": - email_address = oidc.user_getfield("email") - user_name = oidc.user_getfield("preferred_username") - name = oidc.user_getfield("name") - OIDC_NAV_DROPDOWN = renderer.oidc_nav_dropdown(user_name, email_address, name) - OIDC_NAV_MOBILE = renderer.oidc_nav_mobile(user_name, email_address, name) - - return render_template('routes.html', - render_page = renderer.render_routes(), - COLOR_NAV = COLOR_NAV, - COLOR_BTN = COLOR_BTN, - OIDC_NAV_DROPDOWN = OIDC_NAV_DROPDOWN, - OIDC_NAV_MOBILE = OIDC_NAV_MOBILE + @app.route("/routes", methods=("GET", "POST")) + @auth.require_login + @headscale.key_check_guard + async def routes_page(): + return render_template( + "routes.html", + render_page=await renderer.render_routes(headscale), + **render_defaults(), + ) + + @app.route("/machines", methods=("GET", "POST")) + @auth.require_login + @headscale.key_check_guard + async def machines_page(): + return render_template( + "machines.html", + cards=await renderer.render_machines_cards(headscale), + headscale_server=config.hs_server, + inpage_search=renderer.render_search(), + **render_defaults(), + ) + + @app.route("/users", methods=("GET", "POST")) + @auth.require_login + @headscale.key_check_guard + async def users_page(): + return render_template( + "users.html", + cards=await renderer.render_users_cards(headscale), + inpage_search=renderer.render_search(), + ) + + @app.route("/settings", methods=("GET", "POST")) + @auth.require_login + async def settings_page(): + return render_template( + "settings.html", + url=headscale.base_url, + BUILD_DATE=config.build_date, + APP_VERSION=config.app_version, + GIT_REPO_URL=config.git_repo_url, + GIT_COMMIT=config.git_commit, + GIT_BRANCH=config.git_branch, + HS_VERSION=config.hs_version, + **render_defaults(), + ) + + @app.route("/error") + async def error_page(): + """Error page redirect. + + Once we get out of tainted mode, we want to still have this route active so that + users refreshing the page get redirected to the overview page. + """ + return redirect(url_for("overview_page")) + + @app.route("/logout") + @auth.require_login + @headscale.key_check_guard + async def logout_page(): + logout_url = auth.logout() + if logout_url is not None: + return redirect(logout_url) + return redirect(url_for("overview_page")) + + +def register_api_endpoints(app: Flask, headscale: HeadscaleApi, auth: AuthManager): + """Register Headscale WebUI API endpoints.""" + RequestT = TypeVar("RequestT", bound=Message) + ResponseT = TypeVar("ResponseT", bound=Message) + + def api_passthrough( + route: str, + request_type: Type[RequestT], + api_method: Callable[[RequestT], Awaitable[ResponseT | str]], + ): + """Passthrough the Headscale API in a concise form. + + Arguments: + route -- Flask route to the API endpoint. + request_type -- request model (from headscale_api.schema). + api_method -- backend method to pass through the Flask request. + """ + + async def api_passthrough_page(body: RequestT) -> ResponseT | str: + return await api_method(body) # type: ignore + + api_passthrough_page.__name__ = route.replace("/", "_") + api_passthrough_page.__annotations__ = {"body": request_type} + + return app.route(route, methods=["POST"])( + auth.require_login( + headscale.key_check_guard( + validate()(api_passthrough_page) # type: ignore + ) + ) + ) + + class TestKeyRequest(BaseModel): + """/api/test_key request.""" + + api_key: str | None = Field( + None, description="API key to test. If None test the current key." + ) + + @app.route("/api/test_key", methods=("GET", "POST")) + @auth.require_login + @validate() + async def test_key_page(body: TestKeyRequest): + if body.api_key == "": + body.api_key = None + + async with headscale.session: + if not await headscale.test_api_key(body.api_key): + return "Unauthenticated", 401 + + ret = await headscale.renew_api_key() + match ret: + case None: + return "Unauthenticated", 401 + case schema.ApiKey(): + return ret + case _: + new_key_info = await headscale.get_api_key_info() + if new_key_info is None: + return "Unauthenticated", 401 + return new_key_info + + class SaveKeyRequest(BaseModel): + """/api/save_key request.""" + + api_key: str + + @app.route("/api/save_key", methods=["POST"]) + @auth.require_login + @validate() + async def save_key_page(body: SaveKeyRequest): + async with headscale.session: + # Test the new API key. + if not await headscale.test_api_key(body.api_key): + return "Key failed testing. Check your key.", 401 + + try: + headscale.api_key = body.api_key + except OSError: + return "Key did not save properly. Check logs.", 500 + + key_info = await headscale.get_api_key_info() + + if key_info is None: + return "Key saved but error occurred on key info retrieval." + + return ( + f'Key saved and tested: Key: "{key_info.prefix}", ' + f"expiration: {key_info.expiration}" + ) + + #################################################################################### + # Machine API Endpoints + #################################################################################### + + class UpdateRoutePageRequest(BaseModel): + """/api/update_route request.""" + + route_id: int + current_state: bool + + @app.route("/api/update_route", methods=["POST"]) + @auth.require_login + @validate() + async def update_route_page(body: UpdateRoutePageRequest): + if body.current_state: + return await headscale.disable_route( + schema.DisableRouteRequest(body.route_id) + ) + return await headscale.enable_route(schema.EnableRouteRequest(body.route_id)) + + api_passthrough( + "/api/machine_information", + schema.GetMachineRequest, + headscale.get_machine, + ) + api_passthrough( + "/api/delete_machine", + schema.DeleteMachineRequest, + headscale.delete_machine, + ) + api_passthrough( + "/api/rename_machine", + schema.RenameMachineRequest, + headscale.rename_machine, + ) + api_passthrough( + "/api/move_user", + schema.MoveMachineRequest, + headscale.move_machine, + ) + api_passthrough("/api/set_machine_tags", schema.SetTagsRequest, headscale.set_tags) + api_passthrough( + "/api/register_machine", + schema.RegisterMachineRequest, + headscale.register_machine, ) + #################################################################################### + # User API Endpoints + #################################################################################### -@app.route('/machines', methods=('GET', 'POST')) -@oidc.require_login -def machines_page(): - # Some basic sanity checks: - pass_checks = str(helper.load_checks()) - if pass_checks != "Pass": return redirect(url_for(pass_checks)) + api_passthrough("/api/rename_user", schema.RenameUserRequest, headscale.rename_user) + api_passthrough("/api/add_user", schema.CreateUserRequest, headscale.create_user) + api_passthrough("/api/delete_user", schema.DeleteUserRequest, headscale.delete_user) + api_passthrough("/api/get_users", schema.ListUsersRequest, headscale.list_users) - # Check if OIDC is enabled. If it is, display the buttons: - OIDC_NAV_DROPDOWN = Markup("") - OIDC_NAV_MOBILE = Markup("") - INPAGE_SEARCH = Markup(renderer.render_search()) - if AUTH_TYPE == "oidc": - email_address = oidc.user_getfield("email") - user_name = oidc.user_getfield("preferred_username") - name = oidc.user_getfield("name") - OIDC_NAV_DROPDOWN = renderer.oidc_nav_dropdown(user_name, email_address, name) - OIDC_NAV_MOBILE = renderer.oidc_nav_mobile(user_name, email_address, name) - - cards = renderer.render_machines_cards() - return render_template('machines.html', - cards = cards, - headscale_server = headscale.get_url(True), - COLOR_NAV = COLOR_NAV, - COLOR_BTN = COLOR_BTN, - OIDC_NAV_DROPDOWN = OIDC_NAV_DROPDOWN, - OIDC_NAV_MOBILE = OIDC_NAV_MOBILE, - INPAGE_SEARCH = INPAGE_SEARCH + #################################################################################### + # Pre-Auth Key API Endpoints + #################################################################################### + + api_passthrough( + "/api/add_preauth_key", + schema.CreatePreAuthKeyRequest, + headscale.create_pre_auth_key, + ) + api_passthrough( + "/api/expire_preauth_key", + schema.ExpirePreAuthKeyRequest, + headscale.expire_pre_auth_key, + ) + api_passthrough( + "/api/build_preauthkey_table", + schema.ListPreAuthKeysRequest, + functools.partial(renderer.build_preauth_key_table, headscale), ) -@app.route('/users', methods=('GET', 'POST')) -@oidc.require_login -def users_page(): - # Some basic sanity checks: - pass_checks = str(helper.load_checks()) - if pass_checks != "Pass": return redirect(url_for(pass_checks)) + #################################################################################### + # Route API Endpoints + #################################################################################### - # Check if OIDC is enabled. If it is, display the buttons: - OIDC_NAV_DROPDOWN = Markup("") - OIDC_NAV_MOBILE = Markup("") - INPAGE_SEARCH = Markup(renderer.render_search()) - if AUTH_TYPE == "oidc": - email_address = oidc.user_getfield("email") - user_name = oidc.user_getfield("preferred_username") - name = oidc.user_getfield("name") - OIDC_NAV_DROPDOWN = renderer.oidc_nav_dropdown(user_name, email_address, name) - OIDC_NAV_MOBILE = renderer.oidc_nav_mobile(user_name, email_address, name) - - cards = renderer.render_users_cards() - return render_template('users.html', - cards = cards, - COLOR_NAV = COLOR_NAV, - COLOR_BTN = COLOR_BTN, - OIDC_NAV_DROPDOWN = OIDC_NAV_DROPDOWN, - OIDC_NAV_MOBILE = OIDC_NAV_MOBILE, - INPAGE_SEARCH = INPAGE_SEARCH - ) - -@app.route('/settings', methods=('GET', 'POST')) -@oidc.require_login -def settings_page(): - # Some basic sanity checks: - pass_checks = str(helper.load_checks()) - if pass_checks != "Pass" and pass_checks != "settings_page": - return redirect(url_for(pass_checks)) - - # Check if OIDC is enabled. If it is, display the buttons: - OIDC_NAV_DROPDOWN = Markup("") - OIDC_NAV_MOBILE = Markup("") - if AUTH_TYPE == "oidc": - email_address = oidc.user_getfield("email") - user_name = oidc.user_getfield("preferred_username") - name = oidc.user_getfield("name") - OIDC_NAV_DROPDOWN = renderer.oidc_nav_dropdown(user_name, email_address, name) - OIDC_NAV_MOBILE = renderer.oidc_nav_mobile(user_name, email_address, name) - - GIT_COMMIT_LINK = Markup(""+str(os.environ["GIT_COMMIT"])[0:7]+"") - - return render_template('settings.html', - url = headscale.get_url(), - COLOR_NAV = COLOR_NAV, - COLOR_BTN = COLOR_BTN, - OIDC_NAV_DROPDOWN = OIDC_NAV_DROPDOWN, - OIDC_NAV_MOBILE = OIDC_NAV_MOBILE, - BUILD_DATE = os.environ["BUILD_DATE"], - APP_VERSION = os.environ["APP_VERSION"], - GIT_COMMIT = GIT_COMMIT_LINK, - GIT_BRANCH = os.environ["GIT_BRANCH"], - HS_VERSION = os.environ["HS_VERSION"] - ) - -@app.route('/error') -@oidc.require_login -def error_page(): - if helper.access_checks() == "Pass": - return redirect(url_for('overview_page')) - - return render_template('error.html', - ERROR_MESSAGE = Markup(helper.access_checks()) - ) - -@app.route('/logout') -def logout_page(): - if AUTH_TYPE == "oidc": - oidc.logout() - return redirect(url_for('overview_page')) -######################################################################################## -# /api pages -######################################################################################## - -######################################################################################## -# Headscale API Key Endpoints -######################################################################################## - -@app.route('/api/test_key', methods=('GET', 'POST')) -@oidc.require_login -def test_key_page(): - api_key = headscale.get_api_key() - url = headscale.get_url() - - # Test the API key. If the test fails, return a failure. - status = headscale.test_api_key(url, api_key) - if status != 200: return "Unauthenticated" - - renewed = headscale.renew_api_key(url, api_key) - app.logger.warning("The below statement will be TRUE if the key has been renewed, ") - app.logger.warning("or DOES NOT need renewal. False in all other cases") - app.logger.warning("Renewed: "+str(renewed)) - # The key works, let's renew it if it needs it. If it does, re-read the api_key from the file: - if renewed: api_key = headscale.get_api_key() - - key_info = headscale.get_api_key_info(url, api_key) - - # Set the current timezone and local time - timezone = pytz.timezone(os.environ["TZ"] if os.environ["TZ"] else "UTC") - local_time = timezone.localize(datetime.now()) - - # Format the dates for easy readability - creation_parse = parser.parse(key_info['createdAt']) - creation_local = creation_parse.astimezone(timezone) - creation_delta = local_time - creation_local - creation_print = helper.pretty_print_duration(creation_delta) - creation_time = str(creation_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+" "+str(timezone)+" ("+str(creation_print)+")" - - expiration_parse = parser.parse(key_info['expiration']) - expiration_local = expiration_parse.astimezone(timezone) - expiration_delta = expiration_local - local_time - expiration_print = helper.pretty_print_duration(expiration_delta, "expiry") - expiration_time = str(expiration_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+" "+str(timezone)+" ("+str(expiration_print)+")" - - key_info['expiration'] = expiration_time - key_info['createdAt'] = creation_time - - message = json.dumps(key_info) - return message - -@app.route('/api/save_key', methods=['POST']) -@oidc.require_login -def save_key_page(): - json_response = request.get_json() - api_key = json_response['api_key'] - url = headscale.get_url() - file_written = headscale.set_api_key(api_key) - message = '' - - if file_written: - # Re-read the file and get the new API key and test it - api_key = headscale.get_api_key() - test_status = headscale.test_api_key(url, api_key) - if test_status == 200: - key_info = headscale.get_api_key_info(url, api_key) - expiration = key_info['expiration'] - message = "Key: '"+api_key+"', Expiration: "+expiration - # If the key was saved successfully, test it: - return "Key saved and tested: "+message - else: return "Key failed testing. Check your key" - else: return "Key did not save properly. Check logs" - -######################################################################################## -# Machine API Endpoints -######################################################################################## -@app.route('/api/update_route', methods=['POST']) -@oidc.require_login -def update_route_page(): - json_response = request.get_json() - route_id = escape(json_response['route_id']) - url = headscale.get_url() - api_key = headscale.get_api_key() - current_state = json_response['current_state'] - - return headscale.update_route(url, api_key, route_id, current_state) - -@app.route('/api/machine_information', methods=['POST']) -@oidc.require_login -def machine_information_page(): - json_response = request.get_json() - machine_id = escape(json_response['id']) - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.get_machine_info(url, api_key, machine_id) - -@app.route('/api/delete_machine', methods=['POST']) -@oidc.require_login -def delete_machine_page(): - json_response = request.get_json() - machine_id = escape(json_response['id']) - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.delete_machine(url, api_key, machine_id) - -@app.route('/api/rename_machine', methods=['POST']) -@oidc.require_login -def rename_machine_page(): - json_response = request.get_json() - machine_id = escape(json_response['id']) - new_name = escape(json_response['new_name']) - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.rename_machine(url, api_key, machine_id, new_name) - -@app.route('/api/move_user', methods=['POST']) -@oidc.require_login -def move_user_page(): - json_response = request.get_json() - machine_id = escape(json_response['id']) - new_user = escape(json_response['new_user']) - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.move_user(url, api_key, machine_id, new_user) - -@app.route('/api/set_machine_tags', methods=['POST']) -@oidc.require_login -def set_machine_tags(): - json_response = request.get_json() - machine_id = escape(json_response['id']) - machine_tags = json_response['tags_list'] - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.set_machine_tags(url, api_key, machine_id, machine_tags) - -@app.route('/api/register_machine', methods=['POST']) -@oidc.require_login -def register_machine(): - json_response = request.get_json() - machine_key = escape(json_response['key']) - user = escape(json_response['user']) - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.register_machine(url, api_key, machine_key, user) - -######################################################################################## -# User API Endpoints -######################################################################################## -@app.route('/api/rename_user', methods=['POST']) -@oidc.require_login -def rename_user_page(): - json_response = request.get_json() - old_name = escape(json_response['old_name']) - new_name = escape(json_response['new_name']) - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.rename_user(url, api_key, old_name, new_name) - -@app.route('/api/add_user', methods=['POST']) -@oidc.require_login -def add_user(): - json_response = request.get_json() - user_name = str(escape(json_response['name'])) - url = headscale.get_url() - api_key = headscale.get_api_key() - json_string = '{"name": "'+user_name+'"}' - - return headscale.add_user(url, api_key, json_string) - -@app.route('/api/delete_user', methods=['POST']) -@oidc.require_login -def delete_user(): - json_response = request.get_json() - user_name = str(escape(json_response['name'])) - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.delete_user(url, api_key, user_name) - -@app.route('/api/get_users', methods=['POST']) -@oidc.require_login -def get_users_page(): - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.get_users(url, api_key) - -######################################################################################## -# Pre-Auth Key API Endpoints -######################################################################################## -@app.route('/api/add_preauth_key', methods=['POST']) -@oidc.require_login -def add_preauth_key(): - json_response = json.dumps(request.get_json()) - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.add_preauth_key(url, api_key, json_response) - -@app.route('/api/expire_preauth_key', methods=['POST']) -@oidc.require_login -def expire_preauth_key(): - json_response = json.dumps(request.get_json()) - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.expire_preauth_key(url, api_key, json_response) - -@app.route('/api/build_preauthkey_table', methods=['POST']) -@oidc.require_login -def build_preauth_key_table(): - json_response = request.get_json() - user_name = str(escape(json_response['name'])) - - return renderer.build_preauth_key_table(user_name) - -######################################################################################## -# Route API Endpoints -######################################################################################## -@app.route('/api/get_routes', methods=['POST']) -@oidc.require_login -def get_route_info(): - url = headscale.get_url() - api_key = headscale.get_api_key() - - return headscale.get_routes(url, api_key) + api_passthrough("/api/get_routes", schema.GetRoutesRequest, headscale.get_routes) -######################################################################################## -# Main thread -######################################################################################## -if __name__ == '__main__': - app.run(host="0.0.0.0", debug=DEBUG_STATE) +scheduler_registered: bool = False +scheduler_lock = Lock() + + +def register_scheduler(app: Flask, headscale: HeadscaleApi): + """Register background scheduler.""" + global scheduler_registered # pylint: disable=global-statement + + with scheduler_lock: + if scheduler_registered: + # For multi-worker set-up, only a single scheduler needs to be enabled. + return + + scheduler = BackgroundScheduler( + logger=app.logger, timezone=headscale.app_config.timezone + ) + scheduler.start() # type: ignore + + def renew_api_key(): + """Renew API key in a background job.""" + app.logger.info("Key renewal schedule triggered...") + try: + if app.ensure_sync(headscale.renew_api_key)() is None: # type: ignore + app.logger.error("Failed to renew the key. Check configuration.") + except UnauthorizedError: + app.logger.error("Current key is invalid. Check configuration.") + + scheduler.add_job( # type: ignore + renew_api_key, + "interval", + hours=1, + id="renew_api_key", + max_instances=1, + next_run_time=datetime.datetime.now(), + ) + + atexit.register(scheduler.shutdown) # type: ignore + + scheduler_registered = True + + +headscale_webui = asyncio.run(create_app()) + +if __name__ == "__main__": + headscale_webui.run(host="0.0.0.0") diff --git a/static/js/custom.js b/static/js/custom.js index 61c7008..f86595c 100644 --- a/static/js/custom.js +++ b/static/js/custom.js @@ -165,55 +165,54 @@ document.addEventListener('DOMContentLoaded', function () { //----------------------------------------------------------- function test_key() { document.getElementById('test_modal_results').innerHTML = loading() + var api_key = document.getElementById('api_key').value; var data = $.ajax({ - type: "GET", + type: "POST", url: "api/test_key", + data: JSON.stringify({ "api_key": api_key }), + contentType: "application/json", success: function (response) { - if (response == "Unauthenticated") { - html = ` + document.getElementById('test_modal_results').innerHTML = `
    • - warning - Error -

      Key authentication failed. Check your key.

      + check + Success +

      Key authenticated with the Headscale server.

    +
    Key Information
    + + + + + + + + + + + + + + + + + + + +
    Key ID${response.id}
    Prefix${response.prefix}
    Expiration Date${response.expiration}
    Creation Date${response.createdAt}
    ` - document.getElementById('test_modal_results').innerHTML = html - } else { - json = JSON.parse(response) - var html = ` -
      -
    • - check - Success -

      Key authenticated with the Headscale server.

      -
    • -
    -
    Key Information
    - - - - - - - - - - - - - - - - - - - -
    Key ID${json['id']}
    Prefix${json['prefix']}
    Expiration Date${json['expiration']}
    Creation Date${json['createdAt']}
    - ` - document.getElementById('test_modal_results').innerHTML = html - } + }, + error: function (xhr, textStatus, errorThrown) { + document.getElementById('test_modal_results').innerHTML = ` +
      +
    • + warning + Error +

      Key authentication failed. Check your key.

      +
    • +
    + ` } }) @@ -241,7 +240,11 @@ function save_key() { data: JSON.stringify(data), contentType: "application/json", success: function (response) { - M.toast({ html: 'Key saved. Testing...' }); + M.toast({ html: 'Testing key and saving...' }); + test_key(); + }, + error: function (xhr, textStatus, errorThrown) { + M.toast({ html: xhr.responseText }) test_key(); } }) @@ -328,8 +331,8 @@ function load_modal_add_preauth_key(user_name) {

    • Pre-Auth keys can be used to authenticate to Headscale without manually registering a machine. Use the flag --auth-key to do so.
    • -
    • "Ephemeral" keys can be used to register devices that frequently come on and drop off the newtork (for example, docker containers)
    • -
    • Keys that are "Reusable" can be used multiple times. Keys that are "One Time Use" will expire after their first use.
    • +
    • "Ephemeral" keys can be used to register devices that frequently come on and drop off the network (for example, docker containers)
    • +
    • Keys that are "Reusable" can be used multiple times. Keys that are "One Time Use" will expire after their first use.

    @@ -390,7 +393,7 @@ function load_modal_move_machine(machine_id) { document.getElementById('modal_confirm').className = "green btn-flat white-text" document.getElementById('modal_confirm').innerText = "Move" - var data = { "id": machine_id } + var data = { "machine_id": machine_id } $.ajax({ type: "POST", url: "api/machine_information", @@ -400,6 +403,8 @@ function load_modal_move_machine(machine_id) { $.ajax({ type: "POST", url: "api/get_users", + data: "{}", + contentType: "application/json", success: function (response) { modal = document.getElementById('card_modal'); modal_title = document.getElementById('modal_title'); @@ -458,7 +463,7 @@ function load_modal_delete_machine(machine_id) { document.getElementById('modal_confirm').className = "red btn-flat white-text" document.getElementById('modal_confirm').innerText = "Delete" - var data = { "id": machine_id } + var data = { "machine_id": machine_id } $.ajax({ type: "POST", url: "api/machine_information", @@ -508,7 +513,7 @@ function load_modal_rename_machine(machine_id) { document.getElementById('modal_title').innerHTML = "Loading..." document.getElementById('modal_confirm').className = "green btn-flat white-text" document.getElementById('modal_confirm').innerText = "Rename" - var data = { "id": machine_id } + var data = { "machine_id": machine_id } $.ajax({ type: "POST", url: "api/machine_information", @@ -562,6 +567,8 @@ function load_modal_add_machine() { $.ajax({ type: "POST", url: "api/get_users", + data: "{}", + contentType: "application/json", success: function (response) { modal_body = document.getElementById('default_add_new_machine_modal'); modal_confirm = document.getElementById('new_machine_modal_confirm'); @@ -613,8 +620,7 @@ function delete_chip(machine_id, chipsData) { for (let tag in chipsData) { formattedData[tag] = '"tag:' + chipsData[tag].tag + '"' } - var tags_list = '{"tags": [' + formattedData + ']}' - var data = { "id": machine_id, "tags_list": tags_list } + var data = { "machine_id": machine_id, "tags": formattedData } $.ajax({ type: "POST", @@ -636,8 +642,7 @@ function add_chip(machine_id, chipsData) { for (let tag in chipsData) { formattedData[tag] = '"tag:' + chipsData[tag].tag + '"' } - var tags_list = '{"tags": [' + formattedData + ']}' - var data = { "id": machine_id, "tags_list": tags_list } + var data = { "machine_id": machine_id, "tags": formattedData } $.ajax({ type: "POST", @@ -670,19 +675,17 @@ function add_machine() { data: JSON.stringify(data), contentType: "application/json", success: function (response) { - if (response.machine) { - window.location.reload() - return - } - load_modal_generic("error", "Error adding machine", response.message) - return + window.location.reload() + }, + error: function (xhr, textStatus, errorThrown) { + load_modal_generic("error", "Error adding machine", JSON.parse(xhr.responseText).message) } }) } function rename_machine(machine_id) { var new_name = document.getElementById('new_name_form').value; - var data = { "id": machine_id, "new_name": new_name }; + var data = { "machine_id": machine_id, "new_name": new_name }; // String to test against var regexIT = /[`!@#$%^&*()_+\=\[\]{};':"\\|,.<>\/?~]/; @@ -700,24 +703,22 @@ function rename_machine(machine_id) { data: JSON.stringify(data), contentType: "application/json", success: function (response) { + // Get the modal element and close it + modal_element = document.getElementById('card_modal') + M.Modal.getInstance(modal_element).close() - if (response.status == "True") { - // Get the modal element and close it - modal_element = document.getElementById('card_modal') - M.Modal.getInstance(modal_element).close() - - document.getElementById(machine_id + '-name-container').innerHTML = machine_id + ". " + escapeHTML(new_name) - M.toast({ html: 'Machine ' + machine_id + ' renamed to ' + escapeHTML(new_name) }); - } else { - load_modal_generic("error", "Error setting the machine name", "Headscale response: " + JSON.stringify(response.body.message)) - } + document.getElementById(machine_id + '-name-container').innerHTML = machine_id + ". " + escapeHTML(new_name) + M.toast({ html: 'Machine ' + machine_id + ' renamed to ' + escapeHTML(new_name) }); + }, + error: function (xhr, textStatus, errorThrown) { + load_modal_generic("error", "Error setting the machine name", "Headscale response: " + JSON.parse(xhr.responseText).message) } }) } function move_machine(machine_id) { new_user = document.getElementById('move-select').value - var data = { "id": machine_id, "new_user": new_user }; + var data = { "machine_id": machine_id, "user": new_user }; $.ajax({ type: "POST", @@ -742,7 +743,7 @@ function move_machine(machine_id) { } function delete_machine(machine_id) { - var data = { "id": machine_id }; + var data = { "machine_id": machine_id }; $.ajax({ type: "POST", url: "api/delete_machine", @@ -757,6 +758,9 @@ function delete_machine(machine_id) { document.getElementById(machine_id + '-main-collapsible').className = "collapsible popout hide"; M.toast({ html: 'Machine deleted.' }); + }, + error: function (xhr, textStatus, errorThrown) { + load_modal_generic("error", "Error deleting machine", "Headscale response: " + JSON.parse(xhr.responseText).message) } }) } @@ -865,6 +869,7 @@ function get_routes() { async: false, type: "POST", url: "api/get_routes", + data: "{}", contentType: "application/json", success: function (response) { console.log("Got all routes.") @@ -889,8 +894,8 @@ function toggle_failover_route_routespage(routeid, current_state, prefix, route_ var disabledTooltip = "Click to enable" var enabledTooltip = "Click to disable" - var disableState = "False" - var enableState = "True" + var disableState = false + var enableState = true var action_taken = "unchanged." $.ajax({ @@ -1029,25 +1034,24 @@ function rename_user(user_id, old_name) { data: JSON.stringify(data), contentType: "application/json", success: function (response) { - if (response.status == "True") { - // Get the modal element and close it - modal_element = document.getElementById('card_modal') - M.Modal.getInstance(modal_element).close() + // Get the modal element and close it + modal_element = document.getElementById('card_modal') + M.Modal.getInstance(modal_element).close() - // Rename the user on the page: - document.getElementById(user_id + '-name-span').innerHTML = escapeHTML(new_name) + // Rename the user on the page: + document.getElementById(user_id + '-name-span').innerHTML = escapeHTML(new_name) - // Set the button to use the NEW name as the OLD name for both buttons - var rename_button_sm = document.getElementById(user_id + '-rename-user-sm') - rename_button_sm.setAttribute('onclick', 'load_modal_rename_user(' + user_id + ', "' + new_name + '")') - var rename_button_lg = document.getElementById(user_id + '-rename-user-lg') - rename_button_lg.setAttribute('onclick', 'load_modal_rename_user(' + user_id + ', "' + new_name + '")') + // Set the button to use the NEW name as the OLD name for both buttons + var rename_button_sm = document.getElementById(user_id + '-rename-user-sm') + rename_button_sm.setAttribute('onclick', 'load_modal_rename_user(' + user_id + ', "' + new_name + '")') + var rename_button_lg = document.getElementById(user_id + '-rename-user-lg') + rename_button_lg.setAttribute('onclick', 'load_modal_rename_user(' + user_id + ', "' + new_name + '")') - // Send the completion toast - M.toast({ html: "User '" + old_name + "' renamed to '" + new_name + "'." }) - } else { - load_modal_generic("error", "Error setting user name", "Headscale response: " + JSON.stringify(response.body.message)) - } + // Send the completion toast + M.toast({ html: "User '" + old_name + "' renamed to '" + new_name + "'." }) + }, + error: function (xhr, textStatus, errorThrown) { + load_modal_generic("error", "Error setting user name", "Headscale response: " + JSON.parse(xhr.responseText).message) } }) } @@ -1060,19 +1064,17 @@ function delete_user(user_id, user_name) { data: JSON.stringify(data), contentType: "application/json", success: function (response) { - if (response.status == "True") { - // Get the modal element and close it - modal_element = document.getElementById('card_modal') - M.Modal.getInstance(modal_element).close() + // Get the modal element and close it + modal_element = document.getElementById('card_modal') + M.Modal.getInstance(modal_element).close() - // When the machine is deleted, hide its collapsible: - document.getElementById(user_id + '-main-collapsible').className = "collapsible popout hide"; + // When the machine is deleted, hide its collapsible: + document.getElementById(user_id + '-main-collapsible').className = "collapsible popout hide"; - M.toast({ html: 'User deleted.' }); - } else { - // We errored. Decipher the error Headscale sent us and display it: - load_modal_generic("error", "Error deleting user", "Headscale response: " + JSON.stringify(response.body.message)) - } + M.toast({ html: 'User deleted.' }); + }, + error: function (xhr, textStatus, errorThrown) { + load_modal_generic("error", "Error deleting user", "Headscale response: " + JSON.parse(xhr.responseText).message) } }) } @@ -1086,18 +1088,16 @@ function add_user() { data: JSON.stringify(data), contentType: "application/json", success: function (response) { - if (response.status == "True") { - // Get the modal element and close it - modal_element = document.getElementById('card_modal') - M.Modal.getInstance(modal_element).close() + // Get the modal element and close it + modal_element = document.getElementById('card_modal') + M.Modal.getInstance(modal_element).close() - // Send the completion toast - M.toast({ html: "User '" + user_name + "' added to Headscale. Refreshing..." }) - window.location.reload() - } else { - // We errored. Decipher the error Headscale sent us and display it: - load_modal_generic("error", "Error adding user", "Headscale response: " + JSON.stringify(response.body.message)) - } + // Send the completion toast + M.toast({ html: "User '" + user_name + "' added to Headscale. Refreshing..." }) + window.location.reload() + }, + error: function (xhr, textStatus, errorThrown) { + load_modal_generic("error", "Error adding user", "Headscale response: " + JSON.parse(xhr.responseText).message) } }) } @@ -1110,7 +1110,7 @@ function add_preauth_key(user_name) { // If there is no date, error: if (!date) { load_modal_generic("error", "Invalid Date", "Please enter a valid date"); return } - var data = { "user": user_name, "reusable": reusable, "ephemeral": ephemeral, "expiration": expiration } + var data = { "user": user_name, "reusable": reusable, "ephemeral": ephemeral, "expiration": expiration, "acl_tags": [] } $.ajax({ type: "POST", @@ -1118,33 +1118,31 @@ function add_preauth_key(user_name) { data: JSON.stringify(data), contentType: "application/json", success: function (response) { - if (response.status == "True") { - // Send the completion toast - M.toast({ html: 'PreAuth key created in user ' + user_name }) - // If this is successfull, we should reload the table and close the modal: - var user_data = { "name": user_name } - $.ajax({ - type: "POST", - url: "api/build_preauthkey_table", - data: JSON.stringify(user_data), - contentType: "application/json", - success: function (table_data) { - table = document.getElementById(user_name + '-preauth-keys-collection') - table.innerHTML = table_data - // The tooltips need to be re-initialized afterwards: - M.Tooltip.init(document.querySelectorAll('.tooltipped')) - } - }) - // Get the modal element and close it - modal_element = document.getElementById('card_modal') - M.Modal.getInstance(modal_element).close() + // Send the completion toast + M.toast({ html: 'PreAuth key created in user ' + user_name }) + // If this is successful, we should reload the table and close the modal: + var user_data = { "user": user_name } + $.ajax({ + type: "POST", + url: "api/build_preauthkey_table", + data: JSON.stringify(user_data), + contentType: "application/json", + success: function (table_data) { + table = document.getElementById(user_name + '-preauth-keys-collection') + table.innerHTML = table_data + // The tooltips need to be re-initialized afterwards: + M.Tooltip.init(document.querySelectorAll('.tooltipped')) + } + }) + // Get the modal element and close it + modal_element = document.getElementById('card_modal') + M.Modal.getInstance(modal_element).close() - // The tooltips need to be re-initialized afterwards: - M.Tooltip.init(document.querySelectorAll('.tooltipped')) - - } else { - load_modal_generic("error", "Error adding a pre-auth key", "Headscale response: " + JSON.stringify(response.body.message)) - } + // The tooltips need to be re-initialized afterwards: + M.Tooltip.init(document.querySelectorAll('.tooltipped')) + }, + error: function (xhr, textStatus, errorThrown) { + load_modal_generic("error", "Error adding a pre-auth key", "Headscale response: " + JSON.parse(xhr.responseText).message) } }) } @@ -1158,33 +1156,31 @@ function expire_preauth_key(user_name, key) { data: JSON.stringify(data), contentType: "application/json", success: function (response) { - if (response.status == "True") { - // Send the completion toast - M.toast({ html: 'PreAuth expired in ' + user_name }) - // If this is successfull, we should reload the table and close the modal: - var user_data = { "name": user_name } - $.ajax({ - type: "POST", - url: "api/build_preauthkey_table", - data: JSON.stringify(user_data), - contentType: "application/json", - success: function (table_data) { - table = document.getElementById(user_name + '-preauth-keys-collection') - table.innerHTML = table_data - // The tooltips need to be re-initialized afterwards: - M.Tooltip.init(document.querySelectorAll('.tooltipped')) - } - }) - // Get the modal element and close it - modal_element = document.getElementById('card_modal') - M.Modal.getInstance(modal_element).close() + // Send the completion toast + M.toast({ html: 'PreAuth expired in ' + user_name }) + // If this is successful, we should reload the table and close the modal: + var user_data = { "user": user_name } + $.ajax({ + type: "POST", + url: "api/build_preauthkey_table", + data: JSON.stringify(user_data), + contentType: "application/json", + success: function (table_data) { + table = document.getElementById(user_name + '-preauth-keys-collection') + table.innerHTML = table_data + // The tooltips need to be re-initialized afterwards: + M.Tooltip.init(document.querySelectorAll('.tooltipped')) + } + }) + // Get the modal element and close it + modal_element = document.getElementById('card_modal') + M.Modal.getInstance(modal_element).close() - // The tooltips need to be re-initialized afterwards: - M.Tooltip.init(document.querySelectorAll('.tooltipped')) - - } else { - load_modal_generic("error", "Error expiring a pre-auth key", "Headscale response: " + JSON.stringify(response.body.message)) - } + // The tooltips need to be re-initialized afterwards: + M.Tooltip.init(document.querySelectorAll('.tooltipped')) + }, + error: function (xhr, textStatus, errorThrown) { + load_modal_generic("error", "Error expiring a pre-auth key", "Headscale response: " + JSON.parse(xhr.responseText).message) } }) } diff --git a/templates/error.html b/templates/error.html index b08eb4c..80eb878 100644 --- a/templates/error.html +++ b/templates/error.html @@ -23,7 +23,7 @@ - +