diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index e375adb..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 5d4fdf6..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,39 +0,0 @@ -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 9143f53..53bc162 100644 --- a/.gitignore +++ b/.gitignore @@ -1,179 +1,3 @@ -# 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 +__pycache__ .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/ +poetry.lock \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 09f3253..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 5f4545a..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - // 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 deleted file mode 100644 index a9adf05..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "python.analysis.extraPaths": [ - ".venv/src/flask-pydantic/flask_pydantic" - ], - "files.associations": { - "*.html": "jinja" - }, -} diff --git a/Dockerfile b/Dockerfile index 369e73d..28c1cb6 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} @@ -78,4 +78,4 @@ EXPOSE 5000/tcp ENTRYPOINT ["/app/entrypoint.sh"] # Temporarily reduce to 1 worker -CMD gunicorn -w 1 -b 0.0.0.0:5000 server:app +CMD gunicorn -w 1 -b 0.0.0.0:5000 server:app \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index c141d66..ac40d60 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,11 +3,12 @@ pipeline { label 'linux-x64' } environment { - APP_VERSION = 'v0.7.0' + APP_VERSION = 'v0.6.1' HS_VERSION = "v0.21.0" // 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/" @@ -61,7 +62,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 . \ @@ -97,10 +98,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 44de349..b7c8420 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -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 deleted file mode 100644 index 1fe4801..0000000 --- a/auth.py +++ /dev/null @@ -1,242 +0,0 @@ -"""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 deleted file mode 100644 index 886c12f..0000000 --- a/config.py +++ /dev/null @@ -1,545 +0,0 @@ -"""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 50dd6c7..14f01b9 100644 --- a/headscale.py +++ b/headscale.py @@ -1,129 +1,434 @@ -"""Headscale API abstraction.""" - -from functools import wraps -from typing import Awaitable, Callable, ParamSpec, TypeVar +# pylint: disable=wrong-import-order +import requests, json, os, logging, yaml from cryptography.fernet import Fernet -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 +from datetime import timedelta, date +from dateutil import parser +from flask import Flask -from config import Config +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) -T = TypeVar("T") -P = ParamSpec("P") +################################################################## +# 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'] +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("/data/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 -class HeadscaleApi(Headscale): - """Headscale API abstraction.""" +def get_api_key(): + if not os.path.exists("/data/key.txt"): return False + # User-set encryption key + encryption_key = os.environ['KEY'] + # Key file on the filesystem for persistent storage + key_file = open("/data/key.txt", "rb+") + # The encrypted key read from the file + enc_api_key = key_file.read() + if enc_api_key == b'': return "NULL" - def __init__(self, config: Config, requests_timeout: float = 10): - """Initialize the 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() - Arguments: - config -- Headscale WebUI configuration. + return decrypted_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, +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) + } ) + 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 - @property - def app_config(self) -> Config: - """Get Headscale WebUI configuration.""" - return self._config +# 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 hs_config(self) -> HeadscaleConfigBase | None: - """Get Headscale configuration and cache on success. +################################################################## +# Functions related to MACHINES +################################################################## - Returns: - Headscale configuration if a valid configuration has been found. - """ - if self._hs_config is not None: - return self._hs_config +# 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() - 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 - @property - def base_url(self) -> str: - """Get base URL of the Headscale server. +# 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() - 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 +# 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() - return self.hs_config.server_url +def update_route(url, api_key, route_id, current_state): + action = "disable" if current_state == "True" else "enable" - @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 + app.logger.info("Updating Route %s: Action: %s", str(route_id), str(action)) - if not self._config.key_file.exists(): - return None + # 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)) - with open(self._config.key_file, "rb") as key_file: - enc_api_key = key_file.read() - if enc_api_key == b"": - 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() - self._api_key = Fernet(self._config.key).decrypt(enc_api_key).decode() - return self._api_key +# 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() - @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())) +# 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() - # Save to local cache only after successful file write. - self._api_key = new_api_key +# 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()} - 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. +# 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()} - Also, it checks if the key needs renewal and if it is invalid redirects to the - settings page. - """ +# 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() - @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")) +# 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 +################################################################## - return decorated +# 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()} diff --git a/helper.py b/helper.py index b048997..100a623 100644 --- a/helper.py +++ b/helper.py @@ -1,152 +1,301 @@ -"""Helper functions used for formatting.""" +# pylint: disable=wrong-import-order -from datetime import timedelta -from enum import StrEnum -from typing import Literal +import os, headscale, requests, logging +from flask import Flask +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) -def pretty_print_duration( - duration: timedelta, delta_type: Literal["expiry", ""] = "" -): # pylint: disable=too-many-return-statements - """Print a duration in human-readable format.""" +def pretty_print_duration(duration, delta_type=""): + """ Prints a duration in human-readable formats """ 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 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" + 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 "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): + """ Prints a color based on duratioin (imported as seconds) """ - -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() -def get_color(import_id: int, item_type: Literal["failover", "text", ""] = ""): - """Get color for users/namespaces.""" + # 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 """ # Define the colors... Seems like a good number to start with - 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)] + 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] +def format_message(error_type, title, message): + """ Defines a generic 'collection' as error/warning/info messages """ + 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 + data_writable = False # Checks W permissions of /data + data_executable = False # Execute on directories allows file access + file_readable = False # Checks R permissions of /data/key.txt + file_writable = False # Checks W permissions of /data/key.txt + file_exists = False # Checks if /data/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 is rwx for 1000:1000: + if os.access('/data/', os.R_OK): data_readable = True + else: + app.logger.critical("/data READ: FAILED") + checks_passed = False + if os.access('/data/', os.W_OK): data_writable = True + else: + app.logger.critical("/data WRITE: FAILED") + checks_passed = False + if os.access('/data/', os.X_OK): data_executable = True + else: + app.logger.critical("/data EXEC: FAILED") + checks_passed = False + + # Check: /data/key.txt exists and is rw: + if os.access('/data/key.txt', os.F_OK): + file_exists = True + if os.access('/data/key.txt', os.R_OK): file_readable = True + else: + app.logger.critical("/data/key.txt READ: FAILED") + checks_passed = False + if os.access('/data/key.txt', os.W_OK): file_writable = True + else: + app.logger.critical("/data/key.txt WRITE: FAILED") + checks_passed = False + else: app.logger.error("/data/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("/data folder is not writable") + message = """ +

/data is not writable. Please ensure your + permissions are correct. /data mount should be writable + by UID/GID 1000:1000.

+ """ + + message_html += format_message("Error", "/data not writable", message) + + if not data_readable: + app.logger.critical("/data folder is not readable") + message = """ +

/data is not readable. Please ensure your + permissions are correct. /data mount should be readable + by UID/GID 1000:1000.

+ """ + + message_html += format_message("Error", "/data not readable", message) + + if not data_executable: + app.logger.critical("/data folder is not readable") + message = """ +

/data is not executable. Please ensure your + permissions are correct. /data 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", "/data 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("/data/key.txt is not writable") + message = """ +

/data/key.txt is not writable. Please ensure your + permissions are correct. /data mount should be writable + by UID/GID 1000:1000.

+ """ + + message_html += format_message("Error", "/data/key.txt not writable", message) + + if not file_readable: + app.logger.critical("/data/key.txt is not readable") + message = """ +

/data/key.txt is not readable. Please ensure your + permissions are correct. /data mount should be readable + by UID/GID 1000:1000.

+ """ + + message_html += format_message("Error", "/data/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 deleted file mode 100644 index 99f7650..0000000 --- a/poetry.lock +++ /dev/null @@ -1,2267 +0,0 @@ -# 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 deleted file mode 100644 index ab1033b..0000000 --- a/poetry.toml +++ /dev/null @@ -1,2 +0,0 @@ -[virtualenvs] -in-project = true diff --git a/pyproject.toml b/pyproject.toml index 6e61cbb..33a9da2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,47 +1,29 @@ [tool.poetry] name = "headscale-webui" -version = "v0.7.0" +version = "v0.6.1" 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 = {extras = ["async"], version = "^2.2.3"} +Flask = "^2.2.2" 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" -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.dev-dependencies] [tool.poetry.group.dev.dependencies] pylint = "^2.17.0" -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" +autopep8 = "^2.0.2" [build-system] -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" +requires = ["poetry-core>=1.0.0"] \ No newline at end of file diff --git a/renderer.py b/renderer.py index 294be4f..d2ea6fa 100644 --- a/renderer.py +++ b/renderer.py @@ -1,269 +1,224 @@ -# pylint: disable=too-many-lines -"""Page rendering functions. +# pylint: disable=line-too-long, wrong-import-order -TODO: Move some parts to Jinja templates. -""" +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 +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) -import asyncio -import datetime +def render_overview(): + app.logger.info("Rendering the Overview page") + url = headscale.get_url() + api_key = headscale.get_api_key() -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 + 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) -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 - ] - ) + # 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"]) # 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: - 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 - ) + 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 # Get a count of all enabled exit routes exits_count = 0 exits_enabled_count = 0 - 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: + 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"]: exits_enabled_count += 1 # Get User and PreAuth Key counts - 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 - ) + 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 - # Start putting the content together - 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 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"]) - # 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 - - 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 "" - ) + # 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. - 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 "" - ) + 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"]) - 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 "" - ) + # 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)+"""
  • +
+
+
+
+ """ + 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 +"""
  • +
+
+
+
+ """ - # TODO: + # 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 = "" + + # 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 derp is not None and derp.paths is not None: - # pass + if config_yaml["derp"]["paths"]: 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 @@ -272,44 +227,27 @@ async def render_overview(headscale: HeadscaleApi): # pylint: disable=too-many- # The log level # What kind of Database is being used to drive headscale - return Markup( - "
" - + overview_content - + general_content - + derp_content - + oidc_content - + dns_content - ) + content = "
" + overview_content + general_content + derp_content + oidc_content + dns_content + "" + return Markup(content) - -async def thread_machine_content( # pylint: disable=all - headscale: HeadscaleApi, - machine: schema.Machine, - idx: int, - all_routes: schema.GetRoutesResponse, -) -> str: - """Render a single machine.""" +def thread_machine_content(machine, machine_content, idx, all_routes, failover_pair_prefixes): # machine = passed in machine information # content = place to write the content - 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, - ) + # 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() # Set the current timezone and local time - timezone = headscale.app_config.timezone - local_time = datetime.datetime.now(timezone) + timezone = pytz.timezone(os.environ["TZ"] if os.environ["TZ"] else "UTC") + local_time = timezone.localize(datetime.now()) # Get the machines routes - pulled_routes = await headscale.get_machine_routes( - schema.GetMachineRoutesRequest(machine.id) - ) + pulled_routes = headscale.get_machine_routes(url, api_key, machine["id"]) routes = "" # Test if the machine is an exit node: @@ -319,594 +257,445 @@ async def thread_machine_content( # pylint: disable=all ha_enabled = False # If the length of "routes" is NULL/0, there are no routes, enabled or disabled: - 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) + if len(pulled_routes["routes"]) > 0: + advertised_routes = False + # 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

    - """ - # current_app.logger.debug("Pulled Routes Dump: "+str(pulled_routes)) - # current_app.logger.debug("All Routes Dump: "+str(all_routes)) + """ + # app.logger.debug("Pulled Routes Dump: "+str(pulled_routes)) + # app.logger.debug("All Routes Dump: "+str(all_routes)) # Find all exits and put their ID's into the exit_routes array - exit_routes: list[int] = [] + exit_routes = [] exit_enabled_color = "red" exit_tooltip = "enable" exit_route_enabled = False - - for route in pulled_routes.routes: - if route.prefix in ("0.0.0.0/0", "::/0"): - exit_routes.append(route.id) + + for route in pulled_routes["routes"]: + if route["prefix"] == "0.0.0.0/0" or route["prefix"] == "::/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 - 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, - ) + 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)) # Print the button for the Exit routes: if exit_route_found: - routes += ( - f"

    " - "Exit Route

    " - ) + routes = routes+"""

    + 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: - 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." - ) + 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.") # ... 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. - current_app.logger.info( - "New HA pair found: %s", route.prefix - ) - failover_pair_prefixes.append(route.prefix) - if route.enabled and route_info.enabled: + 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"]: # If it is already in the array. . . # Show as HA only if both routes are enabled: - current_app.logger.debug( - "Both routes are enabled. Setting as HA [%s] (%s) ", - machine.name, - route.prefix, - ) + app.logger.debug("Both routes are enabled. Setting as HA [%s] (%s) ", str(machine["name"]), str(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 not in ("0.0.0.0/0", "::/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"] != "0.0.0.0/0" and route["prefix"] != "::/0" and route["prefix"] in failover_pair_prefixes: route_enabled = "red" - route_tooltip = "enable" - color_index = failover_pair_prefixes.index(route.prefix) + route_tooltip = 'enable' + color_index = failover_pair_prefixes.index(str(route["prefix"])) route_enabled_color = helper.get_color(color_index, "failover") - if route.enabled: - color_index = failover_pair_prefixes.index(route.prefix) + if route["enabled"]: + color_index = failover_pair_prefixes.index(str(route["prefix"])) route_enabled = helper.get_color(color_index, "failover") - route_tooltip = "disable" - routes += ( - f"

    " - f"{route.prefix}

    " - ) - + route_tooltip = 'disable' + routes = routes+"""

    + """+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 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, - ) + 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'])) route_enabled = "red" - route_tooltip = "enable" - if route.enabled: + route_tooltip = 'enable' + if route["enabled"]: route_enabled = "green" - route_tooltip = "disable" - routes += ( - f"

    {route.prefix}

    " - ) - routes += "

  • " + route_tooltip = 'disable' + routes = routes+"""

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

    + """ + routes = routes+"

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

    +

  • """ # Get the machine IP's - machine_ips = ( - "" - ) + machine_ips = "" # Format the dates for easy readability - 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})" - ) + 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)+")" - 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})" - ) + 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 there is no expiration date, we don't need to do any calculations: - 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})" - ) + 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)+")" - 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, - ) + 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)) else: - expiry_time = "No expiration date." + expiry_time = "No expiration date." expiring_soon = False - current_app.logger.debug("Machine: %s has no expiration date", machine.name) + app.logger.debug("Machine: "+machine["name"]+" has no expiration date") + # Get the first 10 characters of the PreAuth Key: - if machine.pre_auth_key is not None: - preauth_key = machine.pre_auth_key.key[0:10] - else: - preauth_key = "None" + if machine["preAuthKey"]: + preauth_key = str(machine["preAuthKey"]["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 = ( - 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!" - ) - ) + 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!" - 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, - ) + 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)) +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) -async def render_machines_cards(headscale: HeadscaleApi): - """Render machine cards.""" - current_app.logger.info("Rendering machine cards") + ######################################### + # 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 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("") + # Get all routes + all_routes = headscale.get_routes(url, api_key) + # app.logger.debug("All found routes") + # app.logger.debug(str(all_routes)) + 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") -async def render_users_cards(headscale: HeadscaleApi): - """Render users cards.""" - current_app.logger.info("Rendering Users cards") + # Sort the content by machine_id: + sorted_machines = {key: val for key, val in sorted(machine_content.items(), key = lambda ele: ele[0])} - async with headscale.session: - content = await asyncio.gather( - *[ - build_user_card(headscale, user) - for user in ( - await headscale.list_users(schema.ListUsersRequest()) - ).users - ] - ) + 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) - ) + return Markup(content) - # Set the user badge color: - user_color = helper.get_color(int(user.id), "text") +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) - # Generate the various badges: - status_badge = ( - f"" - "fiber_manual_record" - ) + content = "" + return Markup(content) - preauth_keys = await headscale.list_pre_auth_keys(request) - preauth_keys_collection = f""" -
  • +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 = """
  • 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""" - - - - - - - - - - """ - for key in preauth_keys.pre_auth_keys: + 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"]: # Get the key expiration date and compare it to now to check if it's expired: # Set the current timezone and local time - 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 - ) + 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) + 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 = f"Expiration: {expiration_time}" + btn_delete = "Expire" if key_usable else "" + tooltip_data = "Expiration: "+expiration_time # TR ID will look like "1-albert-tr" - preauth_keys_collection += f""" - - - - - - - - + preauth_keys_collection = preauth_keys_collection+""" + + + + + + + + - """ + """ - return preauth_keys_collection + "
    IDKey Prefix
    Reusable
    Used
    Ephemeral
    Usable
    Actions
    {key.id}{key.key[0:10]}
    {btn_reusable}
    {btn_used}
    {btn_ephemeral}
    {btn_usable}
    {btn_delete}
    """+str(key["id"])+""""""+str(key["key"])[0:10]+"""
    """+btn_reusable+"""
    """+btn_used+"""
    """+btn_ephemeral+"""
    """+btn_usable+"""
    """+btn_delete+"""
  • " + preauth_keys_collection = preauth_keys_collection+""" + + """ + return preauth_keys_collection - -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""" +def oidc_nav_dropdown(user_name, email_address, name): + app.logger.info("OIDC is enabled. Building the OIDC nav dropdown") + html_payload = """
  • - {name} account_circle + """+name+""" account_circle
  • - """ + """ return Markup(html_payload) - -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. +def oidc_nav_mobile(user_name, email_address, name): + html_payload = """ +

  • exit_to_appLogout
  • """ - 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, - } - + return Markup(html_payload) def render_search(): - """Render search bar.""" - return Markup( - """ -
  • - search -
  • - """ - ) + html_payload = """ +
  • + search +
  • + """ + return Markup(html_payload) - -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()) +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) # If there are no routes, just exit: - if len(all_routes.routes) == 0: - return Markup("


    There are no routes to display!
    ") + if len(all_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: 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) + 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"])) - 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 = """
    @@ -915,7 +704,7 @@ async def render_routes(
    """ - markup_post = """ + markup_post = """
    @@ -925,193 +714,146 @@ async def render_routes( ############################################################################################## # Step 1: Get all non-exit and non-failover routes: - route_content = ( - markup_pre - + route_title - + """ -

    - - - - - - - - - - """ - ) - for route in all_routes.routes: + route_content = markup_pre+route_title + route_content += """

    ID Machine Route Enabled
    + + + + + + + + + + """ + for route in all_routes["routes"]: # Get relevant info: - machine = route.machine.given_name - prefix = route.prefix - is_enabled = route.enabled - is_primary = route.is_primary + route_id = route["id"] + machine = route["machine"]["givenName"] + prefix = route["prefix"] + is_enabled = route["enabled"] + is_primary = route["isPrimary"] is_failover = False - is_exit = False + is_exit = False - enabled = ( - f"fiber_manual_record" - ) - disabled = ( - f"fiber_manual_record" - ) + enabled = "fiber_manual_record" + disabled = "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 in ("0.0.0.0/0", "::/0"): - is_exit = True + if prefix == "0.0.0.0/0" or prefix == "::/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 - and route.prefix == route_check.prefix - and route.id != route_check.id - ): - is_failover = True + 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 if not is_exit and not is_failover and machine != "": - # Build a simple table for all non-exit routes: - route_content += f""" - - - - - """ - route_content += "
    ID Machine Route Enabled
    {route.id}{machine}{prefix}
    {enabled_display}

    " + markup_post + # Build a simple table for all non-exit routes: + route_content += """ + + """+str(route_id )+""" + """+str(machine )+""" + """+str(prefix )+""" +
    """+str(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 - # 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 - ) - ) + 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 - if len(failover_route_prefix) > 0: + if failover_available: # 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 = [ - route.id for route in all_routes.routes if route.prefix == route_prefix - ] + route_id_list = [] + for route in all_routes["routes"]: + if route["prefix"] == route_prefix: + route_id_list.append(route["id"]) # Set up the display code: - failover_enabled = ( - f"fiber_manual_record" - ) - failover_disabled = ( - f"fiber_manual_record" - ) + failover_enabled = "fiber_manual_record" + failover_disabled = "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 += f"""

    -

    {failover_display}
    {route_prefix}
    - - - - - - - - - - """ + failover_content += """

    +

    """+failover_display+"""
    """+str(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.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 + 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"] - 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])) + 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])) # Set up the display code: - 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" - ) - + enabled_display_enabled = "fiber_manual_record" + enabled_display_disabled = "fiber_manual_record" + primary_display_enabled = "fiber_manual_record" + primary_display_disabled = "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 += f""" + failover_content += """ - - - + + + """ failover_content += "
    MachineEnabledPrimary
    {machine}
    {enabled_display}
    {primary_display}
    """+str(machine)+"""
    """+str(enabled_display)+"""
    """+str(primary_display)+"""

    " @@ -1119,72 +861,55 @@ async def render_routes( ############################################################################################## # Step 3: Get exit nodes only: - # 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_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"]) # Exit node display building: # Display by machine, not by route - exit_content = ( - markup_pre - + exit_title - + """ -

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

    MachineEnabled
    + + + + + + + + """ + # Get exit route ID's for each node in the list: for node in exit_node_list: - node_exit_route_ids: list[int] = [] + node_exit_route_ids = [] exit_enabled = False exit_available = False machine_id = 0 - 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 + 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 if exit_available: # Set up the display code: - enabled = ( - f"fiber_manual_record" - ) - disabled = ( - f"fiber_manual_record" - ) + enabled = "fiber_manual_record" + disabled = "fiber_manual_record" # Set the displays: enabled_display = enabled if exit_enabled else disabled - exit_content += f""" - - - - - """ - exit_content += "
    MachineEnabled
    {node}
    {enabled_display}

    " + markup_post + exit_content += """ + + """+str(node)+""" +
    """+str(enabled_display)+"""
    + + """ + exit_content += "

    "+markup_post content = route_content + failover_content + exit_content - return Markup(content) + return Markup(content) \ No newline at end of file diff --git a/server.py b/server.py index 3aab318..85c0b79 100644 --- a/server.py +++ b/server.py @@ -1,399 +1,532 @@ -"""Headscale WebUI Flask server.""" +# pylint: disable=wrong-import-order -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 +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 from werkzeug.middleware.proxy_fix import ProxyFix -import renderer -from auth import AuthManager -from config import Config, InitCheckError -from headscale import HeadscaleApi +# 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 +# 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 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." +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"], + } + } ) - @app.route("/") - def catchall_redirect(path: str): # pylint: disable=unused-argument - return redirect(url_for("error_page")) + 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("/error") - async def error_page(): - return render_template( - "error.html", - error_message=Markup( - "".join(sub_error.format_message() for sub_error in error) - ), - ) +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 - return app + 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 + 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() -async def create_app() -> Flask: - """Run Headscale WebUI Flask application. +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() - 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 +######################################################################################## +# / 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)) - with app.app_context(): - # Try to create authentication handler (including loading auth config). - auth = AuthManager(config) + # 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) - # 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 + 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 ) - @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(), - ) +@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("/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, + # 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 ) - #################################################################################### - # User API Endpoints - #################################################################################### - 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) +@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)) - #################################################################################### - # 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), + # 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 ) - #################################################################################### - # Route API Endpoints - #################################################################################### +@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)) - api_passthrough("/api/get_routes", schema.GetRoutesRequest, headscale.get_routes) + # 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) -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") +######################################################################################## +# Main thread +######################################################################################## +if __name__ == '__main__': + app.run(host="0.0.0.0", debug=DEBUG_STATE) diff --git a/static/js/custom.js b/static/js/custom.js index f86595c..3a34636 100644 --- a/static/js/custom.js +++ b/static/js/custom.js @@ -165,54 +165,55 @@ 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: "POST", + type: "GET", url: "api/test_key", - data: JSON.stringify({ "api_key": api_key }), - contentType: "application/json", success: function (response) { - document.getElementById('test_modal_results').innerHTML = ` + if (response == "Unauthenticated") { + html = ` -
    Key Information
    - - - - - - - - - - - - - - - - - - - -
    Key ID${response.id}
    Prefix${response.prefix}
    Expiration Date${response.expiration}
    Creation Date${response.createdAt}
    ` - }, - error: function (xhr, textStatus, errorThrown) { - document.getElementById('test_modal_results').innerHTML = ` - - ` + document.getElementById('test_modal_results').innerHTML = html + } else { + json = JSON.parse(response) + var html = ` + +
    Key Information
    + + + + + + + + + + + + + + + + + + + +
    Key ID${json['id']}
    Prefix${json['prefix']}
    Expiration Date${json['expiration']}
    Creation Date${json['createdAt']}
    + ` + document.getElementById('test_modal_results').innerHTML = html + } } }) @@ -240,11 +241,7 @@ function save_key() { data: JSON.stringify(data), contentType: "application/json", success: function (response) { - M.toast({ html: 'Testing key and saving...' }); - test_key(); - }, - error: function (xhr, textStatus, errorThrown) { - M.toast({ html: xhr.responseText }) + M.toast({ html: 'Key saved. Testing...' }); test_key(); } }) @@ -331,8 +328,8 @@ function load_modal_add_preauth_key(user_name) {

    @@ -393,7 +390,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 = { "machine_id": machine_id } + var data = { "id": machine_id } $.ajax({ type: "POST", url: "api/machine_information", @@ -403,8 +400,6 @@ 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'); @@ -463,7 +458,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 = { "machine_id": machine_id } + var data = { "id": machine_id } $.ajax({ type: "POST", url: "api/machine_information", @@ -513,7 +508,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 = { "machine_id": machine_id } + var data = { "id": machine_id } $.ajax({ type: "POST", url: "api/machine_information", @@ -567,8 +562,6 @@ 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'); @@ -620,7 +613,8 @@ function delete_chip(machine_id, chipsData) { for (let tag in chipsData) { formattedData[tag] = '"tag:' + chipsData[tag].tag + '"' } - var data = { "machine_id": machine_id, "tags": formattedData } + var tags_list = '{"tags": [' + formattedData + ']}' + var data = { "id": machine_id, "tags_list": tags_list } $.ajax({ type: "POST", @@ -642,7 +636,8 @@ function add_chip(machine_id, chipsData) { for (let tag in chipsData) { formattedData[tag] = '"tag:' + chipsData[tag].tag + '"' } - var data = { "machine_id": machine_id, "tags": formattedData } + var tags_list = '{"tags": [' + formattedData + ']}' + var data = { "id": machine_id, "tags_list": tags_list } $.ajax({ type: "POST", @@ -675,17 +670,18 @@ function add_machine() { data: JSON.stringify(data), contentType: "application/json", success: function (response) { - window.location.reload() - }, - error: function (xhr, textStatus, errorThrown) { - load_modal_generic("error", "Error adding machine", JSON.parse(xhr.responseText).message) + if (response.machine) { + window.location.reload() + } + load_modal_generic("error", "Error adding machine", response.message) + return } }) } function rename_machine(machine_id) { var new_name = document.getElementById('new_name_form').value; - var data = { "machine_id": machine_id, "new_name": new_name }; + var data = { "id": machine_id, "new_name": new_name }; // String to test against var regexIT = /[`!@#$%^&*()_+\=\[\]{};':"\\|,.<>\/?~]/; @@ -703,22 +699,24 @@ 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() - 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) + 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)) + } } }) } function move_machine(machine_id) { new_user = document.getElementById('move-select').value - var data = { "machine_id": machine_id, "user": new_user }; + var data = { "id": machine_id, "new_user": new_user }; $.ajax({ type: "POST", @@ -743,7 +741,7 @@ function move_machine(machine_id) { } function delete_machine(machine_id) { - var data = { "machine_id": machine_id }; + var data = { "id": machine_id }; $.ajax({ type: "POST", url: "api/delete_machine", @@ -758,9 +756,6 @@ 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) } }) } @@ -869,7 +864,6 @@ function get_routes() { async: false, type: "POST", url: "api/get_routes", - data: "{}", contentType: "application/json", success: function (response) { console.log("Got all routes.") @@ -894,8 +888,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({ @@ -1034,24 +1028,25 @@ function rename_user(user_id, old_name) { 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() - // 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 + "'." }) - }, - error: function (xhr, textStatus, errorThrown) { - load_modal_generic("error", "Error setting user name", "Headscale response: " + JSON.parse(xhr.responseText).message) + // 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)) + } } }) } @@ -1064,17 +1059,19 @@ function delete_user(user_id, user_name) { 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() - // 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.' }); - }, - error: function (xhr, textStatus, errorThrown) { - load_modal_generic("error", "Error deleting user", "Headscale response: " + JSON.parse(xhr.responseText).message) + 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)) + } } }) } @@ -1088,16 +1085,18 @@ function add_user() { 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() - // 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) + // 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)) + } } }) } @@ -1110,7 +1109,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, "acl_tags": [] } + var data = { "user": user_name, "reusable": reusable, "ephemeral": ephemeral, "expiration": expiration } $.ajax({ type: "POST", @@ -1118,31 +1117,33 @@ function add_preauth_key(user_name) { data: JSON.stringify(data), contentType: "application/json", success: function (response) { - // 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() + 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() - // 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) + // 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)) + } } }) } @@ -1156,31 +1157,33 @@ function expire_preauth_key(user_name, key) { data: JSON.stringify(data), contentType: "application/json", success: function (response) { - // 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() + 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() - // 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) + // 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)) + } } }) } diff --git a/templates/error.html b/templates/error.html index 80eb878..b08eb4c 100644 --- a/templates/error.html +++ b/templates/error.html @@ -23,7 +23,7 @@ - +