diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 510526b2..c42b10c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,26 @@ jobs: format: golang continue-on-error: true + test-revproxy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions/setup-go@v3 + with: + go-version: 1.21 + - name: Test + run: | + make tests + - name: Coveralls + uses: coverallsapp/github-action@v2 + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + COVERALLS_SERVICE_NAME: gihub-action + with: + file: covprofile + format: golang + continue-on-error: true + publish-images: runs-on: ubuntu-latest diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c4953925..7193eee9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,6 +29,11 @@ jobs: contents: read security-events: write + strategy: + fail-fast: false + matrix: + language: [ 'python', 'go' ] + steps: - name: Checkout repository uses: actions/checkout@v2 @@ -37,7 +42,7 @@ jobs: - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: - languages: "go" + languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. diff --git a/Dockerfile b/Dockerfile index 42deb2f8..67775d6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,22 @@ -FROM golang:1.21.6-alpine3.19 as builder -WORKDIR /src -COPY go.mod go.sum ./ -RUN go mod download -COPY cmd/gateway cmd/gateway -COPY internal internal -RUN go build -o /gateway github.com/SwissDataScienceCenter/renku-gateway/cmd/gateway +FROM python:3.11-slim-bookworm as builder +WORKDIR /code +RUN pip install --upgrade pip && \ + pip install poetry && \ + virtualenv .venv +COPY pyproject.toml poetry.lock ./ +RUN poetry install --without dev --no-root +COPY app ./app +RUN poetry install --without dev -FROM alpine:3.19 +FROM python:3.11-slim-bookworm +WORKDIR /code +ENV TINI_VERSION v0.19.0 +ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini +RUN chmod a+x /tini && \ + addgroup renku --gid 1000 && \ + adduser renku --uid 1000 --gid 1000 +COPY --chown=1000:1000 --from=builder /code/.venv .venv +COPY --chown=1000:1000 --from=builder /code/app app USER 1000:1000 -COPY --from=builder /gateway /gateway -ENTRYPOINT [ "/gateway" ] - +ENTRYPOINT [ "/tini", "-g", "--", "./.venv/bin/gunicorn", "-b", "0.0.0.0:5000", "app:app" ] +EXPOSE 5000 diff --git a/Dockerfile.revproxy b/Dockerfile.revproxy new file mode 100644 index 00000000..42deb2f8 --- /dev/null +++ b/Dockerfile.revproxy @@ -0,0 +1,13 @@ +FROM golang:1.21.6-alpine3.19 as builder +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY cmd/gateway cmd/gateway +COPY internal internal +RUN go build -o /gateway github.com/SwissDataScienceCenter/renku-gateway/cmd/gateway + +FROM alpine:3.19 +USER 1000:1000 +COPY --from=builder /gateway /gateway +ENTRYPOINT [ "/gateway" ] + diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000..682c8299 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017-2019 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Quart initialization.""" + +import json +import logging +import os +import re +import sys + +import jwt +import redis +import requests +import sentry_sdk +from flask import Flask, Response, current_app, jsonify, request +from flask_cors import CORS +from flask_kvsession import KVSessionExtension +from sentry_sdk.integrations.flask import FlaskIntegration +from simplekv.decorator import PrefixDecorator +from simplekv.memory.redisstore import RedisStore + +from . import config +from .auth import ( + cli_auth, + gitlab_auth, + renku_auth, + web, + notebook_auth, + keycloak_gitlab_auth, + search_auth, +) +from .auth.oauth_redis import OAuthRedis +from .auth.utils import decode_keycloak_jwt + +# Wait for the VS Code debugger to attach if requested +# TODO: try using debugpy instead of ptvsd to avoid noreload limitations + +VSCODE_DEBUG = os.environ.get("VSCODE_DEBUG") == "1" +if VSCODE_DEBUG: + import debugpy + + print("Waiting for a debugger to attach") + # 5678 is the default attach port in the VS Code debug configurations + debugpy.listen(("localhost", 5678)) + debugpy.wait_for_client() + breakpoint() + +app = Flask(__name__, static_url_path="/") + +# We activate all log levels and prevent logs from showing twice. +app.logger.setLevel(logging.DEBUG if config.DEBUG else logging.INFO) +app.logger.propagate = False + +# Initialize Sentry when required +if os.environ.get("SENTRY_ENABLED", "").lower() == "true": + try: + sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + integrations=[FlaskIntegration()], + environment=os.environ.get("SENTRY_ENVIRONMENT"), + sample_rate=float(os.environ.get("SENTRY_SAMPLE_RATE", 0.2)), + ) + except Exception: + app.logger.warning("Error while trying to initialize Sentry", exc_info=True) + +app.config.from_object(config) + +CORS( + app, + allow_headers=["X-Requested-With"], + allow_origin=app.config["ALLOW_ORIGIN"], +) + +url_prefix = app.config["SERVICE_PREFIX"] +blueprints = ( + cli_auth.blueprint, + gitlab_auth.blueprint, + web.blueprint, +) + + +@app.before_first_request +def setup_redis_client(): + """Set up a redis connection to the master by querying sentinel.""" + + if "pytest" not in sys.modules: + _config = { + "db": current_app.config["REDIS_DB"], + "password": current_app.config["REDIS_PASSWORD"], + "retry_on_timeout": True, + "health_check_interval": 60, + } + if current_app.config["REDIS_IS_SENTINEL"]: + _sentinel = redis.Sentinel( + [(current_app.config["REDIS_HOST"], current_app.config["REDIS_PORT"])], + sentinel_kwargs={"password": current_app.config["REDIS_PASSWORD"]}, + ) + _client = _sentinel.master_for( + current_app.config["REDIS_MASTER_SET"], **_config + ) + else: + _client = redis.Redis( + host=current_app.config["REDIS_HOST"], + port=current_app.config["REDIS_PORT"], + **_config, + ) + + # Set up the redis store for tokens + current_app.store = OAuthRedis(_client, current_app.config["SECRET_KEY"]) + # We use the same store for sessions. + session_store = PrefixDecorator("sessions_", RedisStore(_client)) + KVSessionExtension(session_store, app) + + +@app.route("/", methods=["GET"]) +def auth(): + if "auth" not in request.args: + return Response("", status=200) + + auths = { + "gitlab": gitlab_auth.GitlabUserToken, + "renku": renku_auth.RenkuCoreAuthHeaders, + "notebook": notebook_auth.NotebookAuthHeaders, + "search": search_auth.SearchHeaders, + "cli-gitlab": cli_auth.RenkuCLIGitlabAuthHeaders, + "keycloak_gitlab": keycloak_gitlab_auth.KeycloakGitlabAuthHeaders, + } + + # Keycloak public key is not defined so error + if current_app.config["OIDC_PUBLIC_KEY"] is None: + response = json.dumps("Ooops, something went wrong internally.") + return Response(response, status=500) + + auth_arg = request.args.get("auth") + headers = dict(request.headers) + + try: + auth = auths[auth_arg]() + + # validate incoming authentication + # it can either be in session-cookie or Authorization header + new_token = web.get_valid_token(headers) + if new_token: + headers["Authorization"] = f"Bearer {new_token}" + + if "Authorization" in headers and "Referer" in headers: + allowed = False + origins = decode_keycloak_jwt(token=headers["Authorization"][7:]).get( + "allowed-origins" + ) + for o in origins: + if re.match(o.replace("*", ".*"), headers["Referer"]): + allowed = True + break + if not allowed: + return ( + jsonify( + { + "error": "origin not allowed: {} not matching {}".format( + headers["Referer"], origins + ) + } + ), + 403, + ) + + # auth processors always assume a valid Authorization in header, if any + headers = auth.process(request, headers) + except jwt.ExpiredSignatureError: + current_app.logger.warning( + f"Error while authenticating request, token expired. Target: {auth_arg}", + exc_info=True, + ) + message = { + "error": "authentication", + "message": "token expired", + "target": auth_arg, + } + return jsonify(message), 401 + except AttributeError as error: + if "access_token" in str(error): + current_app.logger.warning( + ( + "Error while authenticating request, can't " + f"refresh access token. Target: {auth_arg}" + ), + exc_info=True, + ) + message = { + "error": "authentication", + "message": "can't refresh access token", + "target": auth_arg, + } + logging.error(error) + return jsonify(message), 401 + raise + except Exception as error: + current_app.logger.warning( + f"Error while authenticating request, unknown. Target: {auth_arg}", + exc_info=True, + ) + message = { + "error": "authentication", + "message": "unknown", + "target": auth_arg, + } + logging.error(error) + return jsonify(message), 401 + + if ( + "anon-id" not in request.cookies + and request.headers.get("X-Requested-With", "") == "XMLHttpRequest" + and "Authorization" not in headers + ): + resp = Response( + json.dumps({"message": "401 Unauthorized"}), + content_type="application/json", + status=401, + ) + return resp + + return Response( + json.dumps("Up and running"), + headers=headers, + status=200, + ) + + +@app.route("/health", methods=["GET"]) +def healthcheck(): + return Response(json.dumps("Up and running"), status=200) + + +def _join_url_prefix(*parts): + """Join prefixes.""" + parts = [part.strip("/") for part in parts if part and part.strip("/")] + if parts: + return "/" + "/".join(parts) + + +for blueprint in blueprints: + app.register_blueprint( + blueprint, + url_prefix=_join_url_prefix(url_prefix, blueprint.url_prefix), + ) + + +@app.before_request +def load_public_key(): + if current_app.config.get("OIDC_PUBLIC_KEY"): + return + + current_app.logger.info( + "Obtaining public key from {}".format(current_app.config["OIDC_ISSUER"]) + ) + + raw_key = requests.get(current_app.config["OIDC_ISSUER"]).json()["public_key"] + current_app.config[ + "OIDC_PUBLIC_KEY" + ] = "-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----".format(raw_key) + + current_app.logger.info(current_app.config["OIDC_PUBLIC_KEY"]) diff --git a/app/auth/__init__.py b/app/auth/__init__.py new file mode 100644 index 00000000..32450cdc --- /dev/null +++ b/app/auth/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""auth module.""" diff --git a/app/auth/cli_auth.py b/app/auth/cli_auth.py new file mode 100644 index 00000000..d326d7a1 --- /dev/null +++ b/app/auth/cli_auth.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Implement Keycloak authentication workflow for CLI.""" +import base64 +import json +import time +from urllib.parse import urljoin + +from flask import Blueprint, current_app, jsonify, request, session, url_for + +from .gitlab_auth import GL_SUFFIX +from .oauth_provider_app import KeycloakProviderApp +from .utils import ( + get_redis_key_for_cli, + get_redis_key_from_session, + get_redis_key_from_token, + handle_login_request, + handle_token_request, +) + +blueprint = Blueprint("cli_auth", __name__, url_prefix="/auth/cli") + +SCOPE = ["profile", "email", "openid"] + + +class RenkuCLIGitlabAuthHeaders: + def process(self, request, headers): + if not request.authorization: + return headers + + access_token = request.authorization.password + if access_token: + redis_key = get_redis_key_from_token(access_token, key_suffix=GL_SUFFIX) + gitlab_oauth_client = current_app.store.get_oauth_client(redis_key) + if gitlab_oauth_client: + gitlab_access_token = gitlab_oauth_client.access_token + user_pass = f"oauth2:{gitlab_access_token}".encode("utf-8") + basic_auth = base64.b64encode(user_pass).decode("ascii") + headers["Authorization"] = f"Basic {basic_auth}" + + return headers + + +@blueprint.route("/login") +def login(): + provider_app = KeycloakProviderApp( + client_id=current_app.config["CLI_CLIENT_ID"], + client_secret=current_app.config["CLI_CLIENT_SECRET"], + base_url=current_app.config["OIDC_ISSUER"], + ) + return handle_login_request( + provider_app, + urljoin(current_app.config["HOST_NAME"], url_for("cli_auth.token")), + current_app.config["CLI_SUFFIX"], + SCOPE, + ) + + +@blueprint.route("/token") +def token(): + response, _ = handle_token_request(request, current_app.config["CLI_SUFFIX"]) + + client_redis_key = get_redis_key_from_session( + key_suffix=current_app.config["CLI_SUFFIX"] + ) + cli_nonce = session.get("cli_nonce") + if cli_nonce: + server_nonce = session.get("server_nonce") + cli_redis_key = get_redis_key_for_cli(cli_nonce, server_nonce) + login_info = CLILoginInfo(client_redis_key) + current_app.store.set_enc(cli_redis_key, login_info.to_json().encode()) + else: + current_app.logger.warn("cli_auth.token called without cli_nonce") + + return response + + +@blueprint.route("/logout") +def logout(): + return "" + + +class CLILoginInfo: + """Stores some information about CLI login.""" + + def __init__(self, client_redis_key, login_start=None): + self.client_redis_key = client_redis_key + self.login_start = login_start or time.time() + + @classmethod + def from_json(cls, json_string): + """Create an instance from json string.""" + data = json.loads(json_string) + return cls(**data) + + def to_json(self): + """Serialize an instance to json string.""" + data = { + "client_redis_key": self.client_redis_key, + "login_start": self.login_start, + } + return json.dumps(data) + + def is_expired(self): + elapsed = time.time() - self.login_start + return elapsed > current_app.config["CLI_LOGIN_TIMEOUT"] + + +def handle_cli_token_request(request): + """Handle final stage of CLI login.""" + cli_nonce = request.args.get("cli_nonce") + server_nonce = request.args.get("server_nonce") + if not cli_nonce or not server_nonce: + return jsonify({"error": "Required arguments are missing."}), 400 + + cli_redis_key = get_redis_key_for_cli(cli_nonce, server_nonce) + current_app.logger.debug(f"Looking up Keycloak for CLI login request {cli_nonce}") + data = current_app.store.get_enc(cli_redis_key) + if not data: + return jsonify({"error": "Something went wrong internally."}), 500 + current_app.store.delete(cli_redis_key) + + login_info = CLILoginInfo.from_json(data.decode()) + if login_info.is_expired(): + return jsonify({"error": "Session expired."}), 403 + + oauth_client = current_app.store.get_oauth_client( + login_info.client_redis_key, no_refresh=True + ) + if not oauth_client: + return jsonify({"error": "Access token not found."}), 404 + + return jsonify({"access_token": oauth_client.access_token}) diff --git a/app/auth/gitlab_auth.py b/app/auth/gitlab_auth.py new file mode 100644 index 00000000..46f1558a --- /dev/null +++ b/app/auth/gitlab_auth.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Implement GitLab authentication workflow.""" + +import re +from urllib.parse import urljoin + +from flask import ( + Blueprint, + current_app, + jsonify, + redirect, + render_template, + request, + url_for, +) + +from ..config import GL_SUFFIX +from .oauth_client import RenkuWebApplicationClient +from .oauth_provider_app import GitLabProviderApp +from .utils import ( + get_redis_key_from_token, + handle_login_request, + handle_token_request, + keycloak_authenticated, +) + + +blueprint = Blueprint("gitlab_auth", __name__, url_prefix="/auth/gitlab") + + +# Note: GitLab oauth tokens do NOT expire per default +# See https://gitlab.com/gitlab-org/gitlab/-/issues/21745 +# The documentation about this is wrong +# (https://docs.gitlab.com/ce/api/oauth2.html#web-application-flow) + + +class GitlabUserToken: + def process(self, request, headers): + m = re.search( + r"bearer (?P.+)", headers.get("Authorization", ""), re.IGNORECASE + ) + if m: + access_token = m.group("token") + redis_key = get_redis_key_from_token(access_token, key_suffix=GL_SUFFIX) + gitlab_oauth_client = current_app.store.get_oauth_client(redis_key) + + if gitlab_oauth_client: + gitlab_access_token = gitlab_oauth_client.access_token + headers["Authorization"] = f"Bearer {gitlab_access_token}" + else: + current_app.logger.debug( + "No authorization header, returning empty auth headers" + ) + + return headers + + +SCOPE = ["openid", "api", "read_user", "read_repository"] + + +@blueprint.route("/login") +def login(): + provider_app = GitLabProviderApp( + client_id=current_app.config["GITLAB_CLIENT_ID"], + client_secret=current_app.config["GITLAB_CLIENT_SECRET"], + base_url=current_app.config["GITLAB_URL"], + ) + return handle_login_request( + provider_app, + urljoin(current_app.config["HOST_NAME"], url_for("gitlab_auth.token")), + current_app.config["GL_SUFFIX"], + SCOPE, + ) + + +@blueprint.route("/token") +def token(): + response, _ = handle_token_request(request, current_app.config["GL_SUFFIX"]) + return response + + +@blueprint.route("/logout") +def logout(): + logout_url = current_app.config["GITLAB_URL"] + "/users/sign_out" + + # For gitlab versions previous to 12.7.0 we need to redirect the + # browser to the logout url. For versions 12.9.0 and newer the + # browser has to POST a form to the logout url. + if current_app.config["OLD_GITLAB_LOGOUT"]: + response = current_app.make_response(redirect(logout_url)) + else: + response = render_template("gitlab_logout.html", logout_url=logout_url) + + return response + + +@blueprint.route("/exchange", methods=["GET"]) +@keycloak_authenticated +def exchange(*, sub, access_token): + """Exchange a keycloak JWT for a valid Gitlab oauth token.""" + redis_key = get_redis_key_from_token( + access_token, key_suffix=current_app.config["GL_SUFFIX"] + ) + # NOTE: getting the client from the redis store automatically refreshes if needed + gl_oauth_client: RenkuWebApplicationClient = current_app.store.get_oauth_client( + redis_key + ) + return jsonify( + { + "access_token": gl_oauth_client.access_token, + "expires_at": gl_oauth_client._expires_at, + }, + ) diff --git a/app/auth/keycloak_auth.py b/app/auth/keycloak_auth.py new file mode 100644 index 00000000..d36327ed --- /dev/null +++ b/app/auth/keycloak_auth.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class KeycloakAccessToken: + def process(self, request, headers): + return headers diff --git a/app/auth/notebook_auth.py b/app/auth/notebook_auth.py new file mode 100644 index 00000000..a36e9371 --- /dev/null +++ b/app/auth/notebook_auth.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Add the headers for the Renku notebooks service.""" +import json +import re +from base64 import b64encode +from typing import List + +from flask import current_app + +from .gitlab_auth import GL_SUFFIX +from .oauth_client import RenkuWebApplicationClient +from .utils import get_redis_key_from_token, get_or_set_keycloak_client +from ..config import KC_SUFFIX + +# TODO: This is a temporary implementation of the header interface defined in #404 +# TODO: that allowes first clients to transition. + + +def get_git_credentials_header(git_oauth_clients: List[RenkuWebApplicationClient]): + """ + Create the git authentication header as defined in #406 + (https://github.com/SwissDataScienceCenter/renku-gateway/issues/406) + given a list of GitLab/GitHub oauth client objects. + """ + + git_credentials = { + client.provider_app.base_url: { + # TODO: add this information to the provider_app and read it from there. + "provider": "GitLab", + "AuthorizationHeader": f"bearer {client.access_token}", + "AccessTokenExpiresAt": client._expires_at, + } + for client in git_oauth_clients + } + git_credentials_json = json.dumps(git_credentials) + return b64encode(git_credentials_json.encode()).decode("ascii") + + +class NotebookAuthHeaders: + def process(self, request, headers): + m = re.search( + r"bearer (?P.+)", headers.get("Authorization", ""), re.IGNORECASE + ) + if m: + access_token = m.group("token") + + keycloak_redis_key = get_redis_key_from_token( + access_token, key_suffix=KC_SUFFIX + ) + keycloak_oidc_client = get_or_set_keycloak_client(keycloak_redis_key) + gitlab_oauth_client = current_app.store.get_oauth_client( + get_redis_key_from_token(access_token, key_suffix=GL_SUFFIX) + ) + + headers["Renku-Auth-Access-Token"] = access_token + headers["Renku-Auth-Refresh-Token"] = keycloak_oidc_client.refresh_token + headers["Renku-Auth-Id-Token"] = keycloak_oidc_client.token["id_token"] + if gitlab_oauth_client: + headers["Renku-Auth-Git-Credentials"] = get_git_credentials_header( + [gitlab_oauth_client] + ) + else: + headers["Renku-Auth-Anon-Id"] = request.cookies.get("anon-id", "") + + return headers diff --git a/app/auth/oauth_client.py b/app/auth/oauth_client.py new file mode 100644 index 00000000..11c3fa68 --- /dev/null +++ b/app/auth/oauth_client.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module contains the logic for obtaining, managing and refreshing + oauth tokens. +""" + +import json +import time + +from oauthlib.oauth2 import WebApplicationClient +from requests_oauthlib import OAuth2Session + +from .oauth_provider_app import OAuthProviderApp + + +class RenkuWebApplicationClient(WebApplicationClient): + """``WebApplicationClientClass`` enriched with provider/app information and + methods for (de-)serializing and obtaining tokens from the provider.""" + + def __init__( + self, + *args, + provider_app=None, + scope=None, + max_lifetime=None, + _expires_at=None, + **kwargs + ): + super().__init__(provider_app.client_id, *args, **kwargs) + assert isinstance( + provider_app, OAuthProviderApp + ), "provider_app property must be instance of OAuthProviderApp class" + self.provider_app = provider_app + self.scope = scope + self.max_lifetime = max_lifetime + self._expires_at = _expires_at + + def get_authorization_url(self): + """Get the authorization url to redirect the browser to.""" + authorization_url, _, _ = super().prepare_authorization_request( + self.provider_app.authorization_endpoint + ) + return authorization_url + + def fetch_token(self, authorization_response, **kwargs): + """Convenience method for fetching tokens.""" + oauth_session = OAuth2Session(client=self, redirect_uri=self.redirect_url) + oauth_session.fetch_token( + self.provider_app.token_endpoint, + authorization_response=authorization_response, + client_secret=self.provider_app.client_secret, + client_id=self.provider_app.client_id, + include_client_id=True, + **kwargs + ) + self._fix_expiration_time() + + def refresh_access_token(self): + """Convenience method for refreshing tokens.""" + self._expires_at = None + oauth_session = OAuth2Session(client=self) + oauth_session.refresh_token( + self.provider_app.token_endpoint, + client_id=self.provider_app.client_id, + client_secret=self.provider_app.client_secret, + include_client_id=True, + ) + self._fix_expiration_time() + + # Note: Pickling would be much simpler here, but we don't fully control + # what's going to be pickeled, so we choose the safer approach. + def to_json(self): + """Serialize a client into json.""" + serializer_attributes = [ + "token_type", + "access_token", + "refresh_token", + "token", + "scope", + "state", + "code", + "redirect_url", + "max_lifetime", + "expires_in", + "_expires_at", + ] + client_dict = {key: vars(self)[key] for key in serializer_attributes} + client_dict["provider_app"] = self.provider_app.to_json() + return json.dumps(client_dict) + + @classmethod + def from_json(cls, serialized_client): + """De-serialize a client from json.""" + client_dict = json.loads(serialized_client) + client_dict["provider_app"] = OAuthProviderApp.from_json( + client_dict["provider_app"] + ) + return cls(**client_dict) + + def _fix_expiration_time(self): + """Cap a very long (or infinite) token lifetime. Note that we + do not modify the actual token (which is an attribute of the client + object) but instead let the client object expire.""" + if self.max_lifetime and ( + (not self.expires_in) or (self.expires_in > self.max_lifetime) + ): + self._expires_at = int(time.time()) + self.max_lifetime + self.expires_in = self.max_lifetime + + def expires_soon(self): + """Check if the client instance expires soon.""" + return self._expires_at and self._expires_at < time.time() + 180 diff --git a/app/auth/oauth_provider_app.py b/app/auth/oauth_provider_app.py new file mode 100644 index 00000000..1afc5bdd --- /dev/null +++ b/app/auth/oauth_provider_app.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module contains the logic to model a client application registered +with an oauth provider. +""" + +import json + +import requests + +PROVIDER_KINDS = { + "GITLAB": "gitlab", + "KEYCLOAK": "keycloak", +} + + +class OAuthProviderApp: + """A simple class combining some information about the oauth provider and the + application registered with the provider.""" + + def __init__( + self, + kind=None, + base_url=None, + client_id=None, + client_secret=None, + authorization_endpoint=None, + token_endpoint=None, + ): + self.kind = kind + self.base_url = base_url + self.client_id = client_id + self.client_secret = client_secret + self.authorization_endpoint = authorization_endpoint + self.token_endpoint = token_endpoint + + # TODO: Use marshmallow for (de)serialization + # TODO: https://github.com/SwissDataScienceCenter/renku-gateway/issues/231 + def to_json(self): + serializer_attributes = [ + "kind", + "base_url", + "client_id", + "client_secret", + "authorization_endpoint", + "token_endpoint", + ] + provider_app_dict = {key: vars(self)[key] for key in serializer_attributes} + return json.dumps(provider_app_dict) + + @classmethod + def from_dict(cls, provider_app_dict): + return _typecast_provider_app(cls(**provider_app_dict)) + + @classmethod + def from_json(cls, serialized_provider_app): + return cls.from_dict(json.loads(serialized_provider_app)) + + +class GitLabProviderApp(OAuthProviderApp): + def __init__(self, base_url=None, client_id=None, client_secret=None): + super().__init__( + kind=PROVIDER_KINDS["GITLAB"], + base_url=base_url, + client_id=client_id, + client_secret=client_secret, + authorization_endpoint="{}/oauth/authorize".format(base_url), + token_endpoint="{}/oauth/token".format(base_url), + ) + + +class KeycloakProviderApp(OAuthProviderApp): + def __init__(self, base_url=None, client_id=None, client_secret=None): + super().__init__( + kind=PROVIDER_KINDS["KEYCLOAK"], + base_url=base_url, + client_id=client_id, + client_secret=client_secret, + ) + # Fetch the details from Keycloak itself + self.get_config() + + def get_config(self): + """Get the endpoints from the base URL by querying keycloak directly.""" + keycloak_config = requests.get( + "{}/.well-known/openid-configuration".format(self.base_url) + ).json() + self.authorization_endpoint = keycloak_config["authorization_endpoint"] + self.token_endpoint = keycloak_config["token_endpoint"] + + # TODO: Keycloak public key / realm information could be added here. + + +PROVIDERS = { + "gitlab": GitLabProviderApp, + "keycloak": KeycloakProviderApp, +} + + +def _typecast_provider_app(provider_app): + """Cast an OAuthProviderApp object to the correct subclass.""" + if provider_app.kind not in PROVIDERS: + return provider_app + + provider_app.__class__ = PROVIDERS[provider_app.kind] + return provider_app diff --git a/app/auth/oauth_redis.py b/app/auth/oauth_redis.py new file mode 100644 index 00000000..1bf57dcc --- /dev/null +++ b/app/auth/oauth_redis.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module handles the storing of user- and provider specific oauth +client instances in redis. +""" + +import base64 +from typing import Any, Optional + +from cryptography.fernet import Fernet +from flask import current_app +from oauthlib.oauth2.rfc6749.errors import OAuth2Error +from redis import Redis + +from .oauth_client import RenkuWebApplicationClient + + +def create_fernet_key(hex_key): + """Small helper to transform a standard 64 hex character secret + into the required urlsafe base64 encoded 32-bytes which serve + as fernet key.""" + + # Check if we have 32 bytes in hex form + if not len(hex_key) == 64: + raise ValueError("provided key must be 64 characters: {}".format(hex_key)) + try: + int(hex_key, 16) + except ValueError: + raise ValueError("provided key contains non-hex character: {}".format(hex_key)) + + # Convert + return base64.urlsafe_b64encode( + bytes([int(hex_key[i : i + 2], 16) for i in range(0, len(hex_key), 2)]) + ) + + +class OAuthRedis: + """Just a thin wrapper around redis store with extra methods for + setting and getting encrypted serializations of oauth client objects.""" + + def __init__(self, redis_client: Redis, fernet_key: Optional[str] = None): + self._redis_client = redis_client + self._fernet = Fernet(create_fernet_key(fernet_key)) + + def set_enc(self, name, value): + """Set method with encryption.""" + return self._redis_client.set(name, self._fernet.encrypt(value)) + + def get_enc(self, name): + """Get method with decryption.""" + value = self._redis_client.get(name) + return None if value is None else self._fernet.decrypt(value) + + def set_oauth_client(self, name, oauth_client): + """Put a client object into the store.""" + return self.set_enc(name, oauth_client.to_json().encode()) + + def get_oauth_client( + self, name, no_refresh=False + ) -> Optional[RenkuWebApplicationClient]: + """Get a client object from the store, refresh if necessary.""" + value = self.get_enc(name) + if value is None: + return None + + oauth_client = RenkuWebApplicationClient.from_json(value.decode()) + + # We refresh 3 minutes before the token/client actually expires + # to avoid unlucky edge cases. + if not no_refresh and oauth_client.expires_soon(): + try: + # TODO: Change logger to have no dependency on the current_app here. + # https://github.com/SwissDataScienceCenter/renku-gateway/issues/113 + current_app.logger.info("Refreshing {}".format(name)) + oauth_client.refresh_access_token() + self.set_enc(name, oauth_client.to_json().encode()) + except OAuth2Error as e: + current_app.logger.warn( + "Error refreshing tokens: {} " + "Clearing client from redis.".format(e.error) + ) + self.delete(name) + return None + + return oauth_client + + def __repr__(self) -> str: + """Overriden to avoid leaking the encryption key or Redis password.""" + return "OAuthRedis()" + + def __getattr__(self, name: str) -> Any: + return self._redis_client.__getattribute__(name) diff --git a/app/auth/renku_auth.py b/app/auth/renku_auth.py new file mode 100644 index 00000000..7bf30684 --- /dev/null +++ b/app/auth/renku_auth.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Add the headers for the Renku core service.""" + +import re +from base64 import b64encode + +from flask import current_app + +from .gitlab_auth import GL_SUFFIX +from .utils import ( + decode_keycloak_jwt, + get_redis_key_from_token, + get_or_set_keycloak_client, +) +from ..config import KC_SUFFIX + +# TODO: We're using a class here only to have a uniform interface +# with GitlabUserToken and JupyterhubUserToken. This should be refactored. + + +class RenkuCoreAuthHeaders: + def process(self, request, headers): + m = re.search( + r"bearer (?P.+)", headers.get("Authorization", ""), re.IGNORECASE + ) + if m: + access_token = m.group("token") + + keycloak_redis_key = get_redis_key_from_token( + access_token, key_suffix=KC_SUFFIX + ) + keycloak_oidc_client = get_or_set_keycloak_client(keycloak_redis_key) + headers["Renku-User"] = keycloak_oidc_client.token["id_token"] + + gitlab_oauth_client = current_app.store.get_oauth_client( + get_redis_key_from_token(access_token, key_suffix=GL_SUFFIX) + ) + headers["Authorization"] = "Bearer {}".format( + gitlab_oauth_client.access_token + ) + + # TODO: The subsequent information is now included in the JWT sent in the + # TODO: "Renku-User" header. It can be removed once the core-service + # TODO: relies on the new header. + access_token_dict = decode_keycloak_jwt(access_token.encode()) + headers["Renku-user-id"] = access_token_dict["sub"] + headers["Renku-user-email"] = b64encode(access_token_dict["email"].encode()) + headers["Renku-user-fullname"] = b64encode( + access_token_dict["name"].encode() + ) + + else: + pass + + return headers diff --git a/app/auth/search_auth.py b/app/auth/search_auth.py new file mode 100644 index 00000000..1b8358c6 --- /dev/null +++ b/app/auth/search_auth.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Add the headers for the Renku searchservice.""" +import re + +from ..config import KC_SUFFIX +from .utils import get_or_set_keycloak_client, get_redis_key_from_token + + +class SearchHeaders: + def process(self, request, headers): + m = re.search( + r"bearer (?P.+)", headers.get("Authorization", ""), re.IGNORECASE + ) + if m: + access_token = m.group("token") + + keycloak_redis_key = get_redis_key_from_token( + access_token, key_suffix=KC_SUFFIX + ) + keycloak_oidc_client = get_or_set_keycloak_client(keycloak_redis_key) + + headers["Renku-Auth-Id-Token"] = keycloak_oidc_client.token["id_token"] + else: + headers["Renku-Auth-Anon-Id"] = request.cookies.get("anon-id", "") + + return headers diff --git a/app/auth/utils.py b/app/auth/utils.py new file mode 100644 index 00000000..d79ab3fa --- /dev/null +++ b/app/auth/utils.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018-2020 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import hashlib +import random +import re +import secrets +import string +from functools import wraps +from urllib.parse import urljoin + +import jwt +from flask import current_app, redirect, request, session, url_for + +from ..config import KEYCLOAK_JWKS_CLIENT +from .oauth_client import RenkuWebApplicationClient +from .oauth_provider_app import KeycloakProviderApp + +JWT_ALGORITHM = "RS256" +TEMP_SESSION_KEY = "temp_cache_key" + + +def decode_keycloak_jwt(token): + """Decode a keycloak access token (JWT) and check the signature""" + return jwt.decode( + token, + current_app.config["OIDC_PUBLIC_KEY"], + algorithms=[JWT_ALGORITHM], + audience=current_app.config["OIDC_CLIENT_ID"], + ) + + +def _get_redis_key(sub_claim, key_suffix=""): + return "cache_{}_{}".format(sub_claim, key_suffix) + + +def get_redis_key_from_session(key_suffix): + """Create a key for the redis store. + - use 'sub' claim if already present in session + - otherwise use temporary cache key if already present in session + - otherwise use newly created random string and store it + Note that the session is passed through the app context.""" + + if session.get("sub", None): + return _get_redis_key(session["sub"], key_suffix=key_suffix) + + if session.get(TEMP_SESSION_KEY, None): + return session[TEMP_SESSION_KEY] + + random_key = "".join(random.choice(string.ascii_lowercase) for i in range(48)) + session[TEMP_SESSION_KEY] = random_key + return random_key + + +def get_redis_key_from_token(token, key_suffix=""): + """Get the redis store from a keycloak access_token.""" + decoded_token = decode_keycloak_jwt(token) + return _get_redis_key(decoded_token["sub"], key_suffix=key_suffix) + + +def get_redis_key_for_cli(cli_nonce, server_nonce): + """Get the redis store from CLI token and user code.""" + cli_nonce_hash = hashlib.sha256(cli_nonce.encode()).hexdigest() + return f"cli_{cli_nonce_hash}_{server_nonce}" + + +def handle_login_request(provider_app, redirect_path, key_suffix, scope): + """Logic to handle the login requests, avoids duplication""" + oauth_client = RenkuWebApplicationClient( + provider_app=provider_app, + redirect_url=urljoin(current_app.config["HOST_NAME"], redirect_path), + scope=scope, + max_lifetime=None, + ) + authorization_url = oauth_client.get_authorization_url() + redis_key = get_redis_key_from_session(key_suffix=key_suffix) + current_app.store.set_oauth_client(redis_key, oauth_client) + + return current_app.make_response(redirect(authorization_url)) + + +def handle_token_request(request, key_suffix): + """Logic to handle the token requests, avoids duplication""" + redis_key = get_redis_key_from_session(key_suffix=key_suffix) + oauth_client = current_app.store.get_oauth_client(redis_key, no_refresh=True) + oauth_client.fetch_token(request.url) + current_app.store.set_oauth_client(redis_key, oauth_client) + response = current_app.make_response( + redirect( + urljoin(current_app.config["HOST_NAME"], url_for("web_auth.login_next")) + ) + ) + + return response, oauth_client + + +def generate_nonce(n_bits=256): + """Generate a one-time secure key.""" + n_bytes = int(n_bits) // 8 + return secrets.token_hex(n_bytes) + + +def get_or_set_keycloak_client(redis_key: str) -> RenkuWebApplicationClient: + """Check if the specific keycloak client is in Redis. If not there + re-initilize it, store it in Redis and return it.""" + from .web import SCOPE as KEYCLOAK_SCOPE + + oauth_client = current_app.store.get_oauth_client(redis_key) + if oauth_client is None: + keycloak_provider_app = KeycloakProviderApp( + client_id=current_app.config["OIDC_CLIENT_ID"], + client_secret=current_app.config["OIDC_CLIENT_SECRET"], + base_url=current_app.config["OIDC_ISSUER"], + ) + oauth_client = RenkuWebApplicationClient( + provider_app=keycloak_provider_app, + redirect_url=urljoin( + current_app.config["HOST_NAME"], url_for("web_auth.token") + ), + scope=KEYCLOAK_SCOPE, + max_lifetime=None, + ) + current_app.store.set_oauth_client(redis_key, oauth_client) + return oauth_client + + +def keycloak_authenticated(f): + """Looks for a JWT in the Authorization header in the form of a bearer token. + Will raise an exception if the JWT is not valid or it has expired. If the token + is valid, it injects the 'sub' claim of the JWT and the encoded JWT in the function + as a keyword arguments. The names for the arguments are 'sub' and 'access_token' + for the sub-claim and access_token respectively.""" + + @wraps(f) + def decorated(*args, **kwargs): + m = re.search( + r"^bearer (?P.+)", + request.headers.get("Authorization", ""), + re.IGNORECASE, + ) + if m: + access_token = m.group("token") + signing_key = KEYCLOAK_JWKS_CLIENT.get_signing_key_from_jwt(access_token) + data = jwt.decode( + access_token, + key=signing_key.key, + algorithms=[JWT_ALGORITHM], + audience=current_app.config["OIDC_CLIENT_ID"], + ) + return f(*args, **kwargs, sub=data["sub"], access_token=access_token) + + raise Exception("Not authenticated") + + return decorated diff --git a/app/auth/web.py b/app/auth/web.py new file mode 100644 index 00000000..c5b12bf8 --- /dev/null +++ b/app/auth/web.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Web auth routes.""" + +import re +from urllib.parse import urljoin + +from flask import ( + Blueprint, + current_app, + redirect, + render_template, + request, + session, + url_for, +) + +from .cli_auth import handle_cli_token_request +from .oauth_provider_app import KeycloakProviderApp +from .utils import ( + TEMP_SESSION_KEY, + decode_keycloak_jwt, + generate_nonce, + get_redis_key_from_session, + get_redis_key_from_token, + handle_login_request, + handle_token_request, +) + +blueprint = Blueprint("web_auth", __name__, url_prefix="/auth") + +SCOPE = ["profile", "email", "openid", "offline_access"] + + +def get_valid_token(headers): + """Look for a fresh and valid token, first in headers, then in the session.""" + authorization = headers.get("Authorization") + authorization_match = ( + re.search(r"bearer\s+(?P.+)", authorization, re.IGNORECASE) + if authorization + else None + ) + + if authorization_match: + renku_token = authorization_match.group("token") + redis_key = get_redis_key_from_token( + renku_token, key_suffix=current_app.config["CLI_SUFFIX"] + ) + elif headers.get("X-Requested-With") == "XMLHttpRequest" and "sub" in session: + redis_key = get_redis_key_from_session( + key_suffix=current_app.config["KC_SUFFIX"] + ) + else: + return None + + keycloak_oidc_client = current_app.store.get_oauth_client(redis_key) + if hasattr(keycloak_oidc_client, "access_token"): + return keycloak_oidc_client.access_token + current_app.logger.debug( + "The user does not have backend access tokens.", + exc_info=True, + ) + return None + + +LOGIN_SEQUENCE = ( + "web_auth.login", + "cli_auth.login", + "gitlab_auth.login", +) + + +@blueprint.route("/login/next") +def login_next(): + session["login_seq"] += 1 + if session["login_seq"] < len(LOGIN_SEQUENCE): + next_login = LOGIN_SEQUENCE[session["login_seq"]] + return render_template( + "redirect.html", + redirect_url=urljoin(current_app.config["HOST_NAME"], url_for(next_login)), + ) + else: + cli_nonce = session.pop("cli_nonce", None) + if cli_nonce: + server_nonce = session.pop("server_nonce", None) + return render_template("cli_login.html", server_nonce=server_nonce) + + return redirect(session["ui_redirect_url"]) + + +@blueprint.route("/login") +def login(): + """Log in with Keycloak.""" + session.clear() + session["ui_redirect_url"] = request.args.get("redirect_url") + + cli_nonce = request.args.get("cli_nonce") + if cli_nonce: + session["cli_nonce"] = cli_nonce + session["server_nonce"] = generate_nonce() + session["login_seq"] = 0 + else: + session["login_seq"] = 1 + + provider_app = KeycloakProviderApp( + client_id=current_app.config["OIDC_CLIENT_ID"], + client_secret=current_app.config["OIDC_CLIENT_SECRET"], + base_url=current_app.config["OIDC_ISSUER"], + ) + return handle_login_request( + provider_app, + urljoin(current_app.config["HOST_NAME"], url_for("web_auth.token")), + current_app.config["KC_SUFFIX"], + SCOPE, + ) + + +@blueprint.route("/token") +def token(): + response, keycloak_oidc_client = handle_token_request( + request, current_app.config["KC_SUFFIX"] + ) + + # Get rid of the temporarily stored oauth client object in redis + # and the reference to it in the session. + old_redis_key = get_redis_key_from_session( + key_suffix=current_app.config["KC_SUFFIX"] + ) + current_app.store.delete(old_redis_key) + session.pop(TEMP_SESSION_KEY, None) + + # Store the oauth client object in redis under the regular "sub" claim. + session["sub"] = decode_keycloak_jwt(keycloak_oidc_client.access_token)["sub"] + new_redis_key = get_redis_key_from_session( + key_suffix=current_app.config["KC_SUFFIX"] + ) + current_app.store.set_oauth_client(new_redis_key, keycloak_oidc_client) + + return response + + +@blueprint.route("/cli-token") +def info(): + return handle_cli_token_request(request) + + +# @blueprint.route("/user") +# async def user(): +# from .. import store + +# if "token" not in session: +# return await current_app.make_response( +# redirect( +# "{}?redirect_url={}".format( +# url_for("web_auth.login"), quote_plus(url_for("web_auth.user")) +# ) +# ) +# ) +# try: +# a = jwt.decode( +# session["token"], +# current_app.config["OIDC_PUBLIC_KEY"], +# algorithms=JWT_ALGORITHM, +# audience=current_app.config["OIDC_CLIENT_ID"], +# ) # TODO: logout and redirect if fails because of expired + +# return current_app.store.get(get_redis_key(a, "kc_id_token")).decode() + +# except jwt.ExpiredSignatureError: +# return await current_app.make_response( +# redirect( +# "{}?redirect_url={}".format( +# url_for("web_auth.login"), quote_plus(url_for("web_auth.user")) +# ) +# ) +# ) + + +@blueprint.route("/user-profile") +def user_profile(): + return current_app.make_response( + redirect("{}/account?referrer=renku".format(current_app.config["OIDC_ISSUER"])) + ) + + +@blueprint.route("/logout") +def logout(): + redis_key = get_redis_key_from_session(key_suffix=current_app.config["KC_SUFFIX"]) + client = current_app.store.get_oauth_client(redis_key) + id_token = None + if client is not None: + tokens = getattr(client, "token", {}) + id_token = tokens.get("id_token") + + session.clear() + + logout_pages = [] + if current_app.config["LOGOUT_GITLAB_UPON_RENKU_LOGOUT"]: + logout_pages = [ + urljoin(current_app.config["HOST_NAME"], url_for("gitlab_auth.logout")) + ] + logout_pages.append( + f"{current_app.config['OIDC_ISSUER']}/protocol/openid-connect/logout" + ) + if id_token is not None: + logout_pages[-1] += f"?id_token_hint={id_token}" + + return render_template( + "redirect_logout.html", + redirect_url=request.args.get("redirect_url"), + logout_pages=logout_pages, + len=len(logout_pages), + ) diff --git a/app/config.py b/app/config.py new file mode 100644 index 00000000..1c08a570 --- /dev/null +++ b/app/config.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Global settings.""" + +import os +import sys +import warnings + +import jwt +from urllib.parse import urljoin + +# This setting can force tokens to be refreshed in case +# they are issued with a too long or unlimited lifetime. +# This is currently the case for BOTH JupyterHub and GitLab! + +# Note that for a clean "client side token expiration" we will +# need https://gitlab.com/gitlab-org/gitlab/-/issues/17259 to be +# fixed and the implementation of JupyterHub as an OAuth2 provider +# completed. +MAX_ACCESS_TOKEN_LIFETIME = os.environ.get("MAX_ACCESS_TOKEN_LIFETIME", 3600 * 24) + +ANONYMOUS_SESSIONS_ENABLED = ( + os.environ.get("ANONYMOUS_SESSIONS_ENABLED", "false") == "true" +) + +HOST_NAME = os.environ.get("HOST_NAME", "http://gateway.renku.build") + +if "GATEWAY_SECRET_KEY" not in os.environ and "pytest" not in sys.modules: + warnings.warn( + "The environment variable GATEWAY_SECRET_KEY is not set. " + "It is mandatory for securely signing session cookies and " + "encrypting REDIS content." + ) + sys.exit(2) +SECRET_KEY = os.environ.get("GATEWAY_SECRET_KEY") + +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SECURE = HOST_NAME.startswith("https") + +ALLOW_ORIGIN = os.environ.get("GATEWAY_ALLOW_ORIGIN", "").split(",") + +REDIS_HOST = os.environ.get("REDIS_HOST", "renku-redis") +REDIS_IS_SENTINEL = os.environ.get("REDIS_IS_SENTINEL", "") == "true" +REDIS_PORT = os.environ.get("REDIS_PORT", 26379 if REDIS_IS_SENTINEL else 6379) +try: + REDIS_PASSWORD = os.environ["REDIS_PASSWORD"] +except KeyError: + warnings.warn( + "No redis password found. Are you sure you don't need one to access redis?" + ) +REDIS_DB = os.environ.get("REDIS_DB", "0") +REDIS_MASTER_SET = os.environ.get("REDIS_MASTER_SET", "mymaster") + +CLI_CLIENT_ID = os.environ.get("CLI_CLIENT_ID", "renku-cli") +CLI_CLIENT_SECRET = os.environ.get("CLI_CLIENT_SECRET", "") +if not CLI_CLIENT_SECRET: + warnings.warn( + "The environment variable CLI_CLIENT_SECRET is not set." + "It is mandatory for CLI login." + ) + +CLI_LOGIN_TIMEOUT = int(os.environ.get("CLI_LOGIN_TIMEOUT", 300)) + +GITLAB_URL = os.environ.get("GITLAB_URL", "http://gitlab.renku.build") +GITLAB_CLIENT_ID = os.environ.get("GITLAB_CLIENT_ID", "renku-ui") +GITLAB_CLIENT_SECRET = os.environ.get("GITLAB_CLIENT_SECRET") +if not GITLAB_CLIENT_SECRET: + warnings.warn( + "The environment variable GITLAB_CLIENT_SECRET is not set." + "It is mandatory for Gitlab login." + ) + +WEBHOOK_SERVICE_HOSTNAME = os.environ.get( + "WEBHOOK_SERVICE_HOSTNAME", "http://renku-graph-webhooks-service" +) + +KEYCLOAK_URL = os.environ.get("KEYCLOAK_URL", HOST_NAME.strip("/") + "/auth").strip("/") +KEYCLOAK_REALM = os.environ.get("KEYCLOAK_REALM", "Renku") +OIDC_ISSUER = "{}/realms/{}".format(KEYCLOAK_URL, KEYCLOAK_REALM) +OIDC_CLIENT_ID = os.environ.get("OIDC_CLIENT_ID", "renku") +OIDC_CLIENT_SECRET = os.environ.get("OIDC_CLIENT_SECRET") +if not OIDC_CLIENT_SECRET: + warnings.warn( + "The environment variable OIDC_CLIENT_SECRET is not set. " + "It is mandatory for OpenId-Connect login." + ) + +SERVICE_PREFIX = os.environ.get("GATEWAY_SERVICE_PREFIX", "/") + +OLD_GITLAB_LOGOUT = os.environ.get("OLD_GITLAB_LOGOUT", "") == "true" + +LOGOUT_GITLAB_UPON_RENKU_LOGOUT = ( + os.environ.get("LOGOUT_GITLAB_UPON_RENKU_LOGOUT", "") == "true" +) + +KEYCLOAK_JWKS_CLIENT = jwt.PyJWKClient( + urljoin( + KEYCLOAK_URL + "/", f"realms/{KEYCLOAK_REALM}/protocol/openid-connect/certs" + ) +) + +GL_SUFFIX = "gl_oauth_client" +CLI_SUFFIX = "cli_oauth_client" +KC_SUFFIX = "kc_oidc_client" + +DEBUG = os.environ.get("DEBUG", "false") == "true" diff --git a/app/templates/cli_login.html b/app/templates/cli_login.html new file mode 100644 index 00000000..dccfaa75 --- /dev/null +++ b/app/templates/cli_login.html @@ -0,0 +1,28 @@ + + + + + Renku - login + + + +

Login was successful. Copy the following code to your Renku command-line client to complete the login process:

+ {{server_nonce}} +

Note: If you did not initiate a Renku CLI login, do not share this code with any third parties.

+

You can close this window after copying the code.

+ + diff --git a/app/templates/gitlab_logout.html b/app/templates/gitlab_logout.html new file mode 100644 index 00000000..c0328ec1 --- /dev/null +++ b/app/templates/gitlab_logout.html @@ -0,0 +1,34 @@ + + + + + GitLab - logout + + + +

Logging you out from GitLab...

+
+
+ + + + diff --git a/app/templates/redirect.html b/app/templates/redirect.html new file mode 100644 index 00000000..3e51e1f0 --- /dev/null +++ b/app/templates/redirect.html @@ -0,0 +1,61 @@ +{# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + + + + + + + + + + Renku - login + + + + +
+ +
+
+
+

Redirecting...

+

If you are not redirected automatically, follow this link

+
+
+ + + diff --git a/app/templates/redirect_logout.html b/app/templates/redirect_logout.html new file mode 100644 index 00000000..2764d62b --- /dev/null +++ b/app/templates/redirect_logout.html @@ -0,0 +1,63 @@ +{# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + + + + + + + + + Renku - login + + + +
+ +
+
+
+

Redirecting...

+

If you are not redirected in 5 seconds, follow this link.

+ {%for i in range(0, len)%} + + {%endfor%} +
+
+ + + + diff --git a/app/tests/__init__.py b/app/tests/__init__.py new file mode 100644 index 00000000..6796f957 --- /dev/null +++ b/app/tests/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" Enable testing """ diff --git a/app/tests/test_data.py b/app/tests/test_data.py new file mode 100644 index 00000000..717ccd18 --- /dev/null +++ b/app/tests/test_data.py @@ -0,0 +1,281 @@ +# flake8: noqa + +TOKEN_PAYLOAD = { + "jti": "ebb2b1cb-6176-483c-9671-88ced95f9a2f", + "exp": 999999999999, + "nbf": 0, + "iat": 1528894957, + "iss": "http://keycloak.renku.build:8080/auth/realms/Renku", + "aud": "renku", + "sub": "5dbdeba7-e40f-42a7-b46b-6b8a07c65966", + "typ": "Bearer", + "azp": "renku", + "auth_time": 1528894957, + "session_state": "899bfe3c-5a7e-4ea0-b340-b4179b297968", + "acr": "1", + "allowed-origins": ["http://gateway.renku.build/*", "http://localhost:5000/*"], + "realm_access": {"roles": ["uma_authorization"]}, + "resource_access": { + "account": {"roles": ["manage-account", "manage-account-links", "view-profile"]} + }, + "name": "Andreas Bleuler", + "preferred_username": "ableuler", + "given_name": "Andreas", + "family_name": "Bleuler", + "email": "andreas.bleuler@sdsc.ethz.ch", +} + +PRIVATE_KEY = ( + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIJJwIBAAKCAgEAz9NEMFlsDjXOa6VuBjVzvA3tRPJezepJt1CLNxZW1La7xbn1" + "BxPQD1ccmxVNmlSnQd2xkS1oKsv8c7TcZY6mJma6UOKQqsjpI8ghDcCEk+l+xSh8" + "AnuRIm7FQXY9nwdcxZsD+MLW6+HJLE4kAPob8MjV05O6Nh85YF+8CGGW7Iv67gRH" + "FzsaP3ofKcsyngs8vA+z5WpvMhv7NQtbw8uebdpH3sEmGaBYIdVZa5quiwzM1rQL" + "QUw688dxuAFCmv0QpFQfO5lYeuRX9e7gRqDQkjG9nh0hnCKs7GVg8efafBuaUQlg" + "038D2V1tVDWds4n+oICcmfYOUaspIWBa9QKeJiv/8qcdh3GbvNqFPWX/IVmZgrQG" + "Q1c75VfEXY6wdXmQwwmMW+1bjJIRUtO6Z//QdggBrB5BjeS6XWafBVV3XtzkgSYi" + "BmOmSLGlTXULyxhktpddGZO8Bg6HwLa2aVNpneSOqcKkQObzeHKJm35tARGCRy6k" + "bahy1kXnZ5MC1DIHlwHa9DK+TWC7mHC7vFxIAwBEGFMTK5kP7Yg92x67JtjwR6w7" + "T2zNwEfHGge9XwA28VcBUCZqBmeS3vsRw4h6rmLQw3c+CD5jbviv9xMHw/XA77JE" + "Jd1XckKI8vwOI+ZQ2vcqYo2X6CUx5yHjirm2bkr/hHS8VK7rmF9rCD2gQM0CAwEA" + "AQKCAgA3vEkFTnYUOYnqhKtFLwCi5nlDjFywjKzIZOlxFKSk13z0QjLcewvJkWsy" + "jDwLr7hLidEdRjgxghNqVI7nDaKxmctN9fUmWEtuNTXoIkFsCard5UWcxNbfjSWJ" + "sNRF2gufUzt1c4uAJ0V0hGBTgsALi1ENNQkzipwwpHwhI0r+lWvueWc3a7pWW8IP" + "y1b/27OmG+/7DthTb/2m9CzgDbOncmrj6pj1NnNsX3Nj0FAPKpek3RRHptIInux4" + "lJ3wQv47k/PsX+vCyYptgmrThj1pd72KsfVZklMd8vJU7gFCV4TDRuiYz++QU+YG" + "N3rbs55+HP/iqoKclHKraNP78X/H8SSFqhLaOEaTnIugPv5Bb1Bmshp+B9bNya5z" + "8P1V/cwBy+YppkGnvk1Nj1a5+rnVthPioMww/iPHooJ0RtVMXlu7mKZJ5Zl8GCcv" + "rZs6GMPpWQrts7GBCrd0PF8OLhi/dZoFjwBfZfjU4BpA2+HY3+/LZbNFvZVAUpG3" + "YfVf4J82j/UUvmLhJ/4/pNJNIg53hd50AVwxckqXKpsyhJdVQnvIb3bs+lftFTRW" + "picuZ+4NGPKh024wrh/E5cPMEq/dt0EJPkkn10HKqNWI3y6MuIRc2nAu7EFy8WJZ" + "p22HWyZl+oEuIOq193+PfwwebFG7RO08yldIW9dB9C6R48ZRAQKCAQEA79am4OXB" + "SH/tqy7quMaH1lj5Y3OLOTlSyqL16cgvafRWnpEviz8jsQh03nDMwmmv+muEOoy4" + "szfhkuCid3uUsSslEBEXhirMFBUCQsDaJ9xfPJeVWJ92ErAAK6RBHjDW2Acm2jQd" + "VgCJyYX7kNuUS/w1Yp3S8QeuyxXwmcCIUq4WO1Cdxgx9/Pau+lA8DpVTDXqLGJ5Z" + "UeHqfIKVhaSul8Mwh0QOc+dnGQHKU4Pn+v8oNnwwKKk65Hxc6Ej6hEcNG71Gbxk3" + "7XAhppqj+2c1ds1BFFhsiheTuwHnycJccOMiehbZ04XYquRNNOKonCf+LOWeF9sg" + "qs8nihxM1SYJRQKCAQEA3dReUmMO0zhM+9hoqUf5cjaLYCt6cV6o6qLlYU4+6FBX" + "GHvZln97o8lsKiBWlkCqFsTBKjg+fu172h877gpsyHIE+uORlyuHFCLD2A/eWrav" + "uReu5fH2dkz9Mh3Fq9t6hIamUN9TIaqllkRH3Tcqyzk7jGyC7vTMPseIKly4YlWN" + "muhrWR/QZYaT4+w66Lyva00UDDGp3eatystG/9VsV8QteCwXH0fi1m0MM3+pXv3q" + "0Q47SSSYS7uxat1/3MJ7EjFtbXvETvIb4HiTsY/WSZGbwfgsCQTrp7onIs93oYG/" + "AC0o6i6f7vprOHUX03IhBLEe/7gICqRWkMjgMfEd6QKCAQBkV58L+rQJ/BPYidGE" + "KvOL9z+nnyDBeT0tME7IV4uWvbY7syx8CpeJKquSoQjZ0dPhZng08skXmiqTA86V" + "RKvqD8360dvQszkcscl3Wi4rfSSPOjAumtCQcvgvShJAaliImz1jD2iyoZkEKj0c" + "1vFNdSB0uOkXFIrJxs0Z1pZyWQlOGaVYxcM0QZTlfwoRY+ISgpGNZDqkamtrWkrq" + "VgMB1ZUJEq0lSsw0hy46ELbOqVAOs5iGen78Nxe7y0SccQmH8IF2W8utWDuL86jl" + "tsGEic1PkMsgX0rcc6ihHeMFC9JR2BucRqRmowu2M5otcwIBkLO68V/SdsbpHnv6" + "tWYtAoIBAFdXYrv1nMS1ijou/yaH3EOIDmCTPeadaszXzpD9ie9WkrRlL0r+buQS" + "TrBXg0AtvcqxNY02EAVR5E4BtksHd8WEf0l5iL2IuerHtWzA8r+s5otuM8L9/hie" + "P6MX7di41giQK7Pz+ntrAT+lKtaC/ip+ImAr6XHEmRau4YIsd7zgCp1PndS9ngQb" + "dOds/9TbVgZdluMmOsfQJ+WNHCtnEP2NlImYcpIyb7IVxZQRU9K/D1G41Mb7zasj" + "/7sf81QsjuCe7YMKFEUxNqCvWRe0lp7o4fcBi/URJugnd3lRTr0cpOOg5FcwfHBP" + "0R+tmu/6I94BDz+IakImap8fOIbxdOECggEAWuO9u6QAPY3QpNxDmU+JSvCG0i1a" + "7TcCreZTGGENStM+eUt6Qf4KaXq6Ieg8SIWidBJXzgRwlaOX+JNXcgc/xUrZF+5d" + "WqW3p0DXek9880do0nMPcdo/HkjQIl7LHmrfgz8tebc+e/wZ7JkXxt95p8lGNhhi" + "lkqPezoK4fg9hcfMZ5Saey7PKHGaDyYiGszHHKacvEhuzMoD3V+vml2AUkkkGfAA" + "iCWY5aOH1ykv9YA9dfmXG2xqlohUNd0tSOijzbNZD8tA3EcT+gRSHEwE8mtNxlkB" + "dBf9eIbKYcDjFgjPZceifMoQukXgbGP58LBE9UsdiRUUpKjn7GXhxiIdTA==\n" + "-----END RSA PRIVATE KEY-----" +) + +PUBLIC_KEY = ( + "-----BEGIN PUBLIC KEY-----\n" + "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAz9NEMFlsDjXOa6VuBjVz" + "vA3tRPJezepJt1CLNxZW1La7xbn1BxPQD1ccmxVNmlSnQd2xkS1oKsv8c7TcZY6m" + "Jma6UOKQqsjpI8ghDcCEk+l+xSh8AnuRIm7FQXY9nwdcxZsD+MLW6+HJLE4kAPob" + "8MjV05O6Nh85YF+8CGGW7Iv67gRHFzsaP3ofKcsyngs8vA+z5WpvMhv7NQtbw8ue" + "bdpH3sEmGaBYIdVZa5quiwzM1rQLQUw688dxuAFCmv0QpFQfO5lYeuRX9e7gRqDQ" + "kjG9nh0hnCKs7GVg8efafBuaUQlg038D2V1tVDWds4n+oICcmfYOUaspIWBa9QKe" + "Jiv/8qcdh3GbvNqFPWX/IVmZgrQGQ1c75VfEXY6wdXmQwwmMW+1bjJIRUtO6Z//Q" + "dggBrB5BjeS6XWafBVV3XtzkgSYiBmOmSLGlTXULyxhktpddGZO8Bg6HwLa2aVNp" + "neSOqcKkQObzeHKJm35tARGCRy6kbahy1kXnZ5MC1DIHlwHa9DK+TWC7mHC7vFxI" + "AwBEGFMTK5kP7Yg92x67JtjwR6w7T2zNwEfHGge9XwA28VcBUCZqBmeS3vsRw4h6" + "rmLQw3c+CD5jbviv9xMHw/XA77JEJd1XckKI8vwOI+ZQ2vcqYo2X6CUx5yHjirm2" + "bkr/hHS8VK7rmF9rCD2gQM0CAwEAAQ==\n" + "-----END PUBLIC KEY-----" +) + +GITLAB_PROJECTS = [ + { + "id": 1, + "description": "", + "name": "demo", + "name_with_namespace": "John Doe / demo", + "path": "demo", + "path_with_namespace": "demo/demo", + "created_at": "2018-07-20T08:10:28.150Z", + "default_branch": None, + "tag_list": [], + "ssh_url_to_repo": "ssh: //git@gitlab.renku.build: 5022/demo/demo.git", + "http_url_to_repo": "http: //gitlab.renku.build/demo/demo.git", + "web_url": "http: //gitlab.renku.build/demo/demo", + "avatar_url": None, + "star_count": 0, + "forks_count": 0, + "last_activity_at": "2018-07-20T08:10:28.150Z", + "_links": { + "self": "http://gitlab.renku.build/api/v4/projects/1", + "issues": "http://gitlab.renku.build/api/v4/projects/1/issues", + "merge_requests": "http://gitlab.renku.build/api/v4/projects/1/merge_requests", + "repo_branches": "http://gitlab.renku.build/api/v4/projects/1/repository/branches", + "labels": "http://gitlab.renku.build/api/v4/projects/1/labels", + "events": "http://gitlab.renku.build/api/v4/projects/1/events", + "members": "http://gitlab.renku.build/api/v4/projects/1/members", + }, + "archived": False, + "visibility": "public", + "owner": { + "id": 2, + "name": "John Doe", + "username": "demo", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/3279e2b7c1a20fc486a409c7b6280485?s=80\u0026d=identicon", + "web_url": "http://gitlab.renku.build/demo", + }, + "resolve_outdated_diff_discussions": False, + "container_registry_enabled": True, + "issues_enabled": True, + "merge_requests_enabled": True, + "wiki_enabled": True, + "jobs_enabled": True, + "snippets_enabled": True, + "shared_runners_enabled": True, + "lfs_enabled": True, + "creator_id": 2, + "namespace": { + "id": 2, + "name": "demo", + "path": "demo", + "kind": "user", + "full_path": "demo", + "parent_id": None, + }, + "import_status": "none", + "open_issues_count": 1, + "public_jobs": True, + "ci_config_path": None, + "shared_with_groups": [], + "only_allow_merge_if_pipeline_succeeds": False, + "request_access_enabled": False, + "only_allow_merge_if_all_discussions_are_resolved": False, + "printing_merge_request_link_enabled": True, + "permissions": { + "project_access": {"access_level": 40, "notification_level": 3}, + "group_access": None, + }, + } +] + +GITLAB_ISSUES = [ + { + "id": 1, + "iid": 1, + "project_id": 1, + "title": "Ohoh", + "description": "this is not working", + "state": "opened", + "created_at": "2018-07-20T08:20:10.052Z", + "updated_at": "2018-07-20T08:20:10.052Z", + "closed_at": None, + "labels": [], + "milestone": None, + "assignees": [], + "author": { + "id": 2, + "name": "John Doe", + "username": "demo", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/3279e2b7c1a20fc486a409c7b6280485?s=80\u0026d=identicon", + "web_url": "http://gitlab.renku.build/demo", + }, + "assignee": None, + "user_notes_count": 0, + "upvotes": 0, + "downvotes": 0, + "due_date": None, + "confidential": False, + "discussion_locked": None, + "web_url": "http://gitlab.renku.build/demo/demo/issues/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": None, + "human_total_time_spent": None, + }, + } +] + +GATEWAY_PROJECT = [ + { + "display": { + "title": "demo", + "slug": "demo", + "display_id": "demo/demo", + "short_description": "", + }, + "metadata": { + "author": { + "id": 2, + "name": "John Doe", + "username": "demo", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/3279e2b7c1a20fc486a409c7b6280485?s=80&d=identicon", + "web_url": "http://gitlab.renku.build/demo", + }, + "created_at": "2018-07-20T08:10:28.150Z", + "last_activity_at": "2018-07-20T08:10:28.150Z", + "permissions": [], + "id": 1, + }, + "description": "", + "long_description": "test", + "name": "demo", + "forks_count": 0, + "star_count": 0, + "tags": [], + "kus": [ + [ + { + "project_id": 1, + "display": { + "title": "Ohoh", + "slug": 1, + "display_id": 1, + "short_description": "Ohoh", + }, + "metadata": { + "author": { + "id": 2, + "name": "John Doe", + "username": "demo", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/3279e2b7c1a20fc486a409c7b6280485?s=80&d=identicon", + "web_url": "http://gitlab.renku.build/demo", + }, + "created_at": "2018-07-20T08:20:10.052Z", + "updated_at": "2018-07-20T08:20:10.052Z", + "id": 1, + "iid": 1, + }, + "description": "this is not working", + "labels": [], + "contributions": [], + "assignees": [], + "reactions": [], + } + ] + ], + "repository_content": [], + } +] + +SECRET_KEY = "38d5affe8e8719f5e0b355da459b66fa120455324d7f4a232b2267f606a335e4" + +PROVIDER_APP_DICT = { + "client_id": "someId", + "client_secret": "someSecret", + "base_url": "someUrl", +} + +REDIS_PASSWORD = "bf4c75968fb7e23575b9b78fa0b375407d2c1e477782335bc764146cd2965d9c" diff --git a/app/tests/test_proxy.py b/app/tests/test_proxy.py new file mode 100644 index 00000000..5c5dd9ae --- /dev/null +++ b/app/tests/test_proxy.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 - Swiss Data Science Center (SDSC) +# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and +# Eidgenössische Technische Hochschule Zürich (ETHZ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" Test for the proxy """ + +import jwt +import pytest +import requests +import responses + +from fakeredis import FakeStrictRedis + +from .. import app +from ..auth.oauth_client import RenkuWebApplicationClient +from ..auth.oauth_provider_app import OAuthProviderApp +from ..auth.oauth_redis import OAuthRedis +from ..auth.utils import get_redis_key_from_token +from .test_data import ( + PRIVATE_KEY, + PROVIDER_APP_DICT, + PUBLIC_KEY, + SECRET_KEY, + TOKEN_PAYLOAD, + REDIS_PASSWORD, +) + +# TODO: Completely refactor all tests, massively improve test coverage. +# TODO: https://github.com/swissdatasciencecenter/renku-gateway/issues/92 + + +@pytest.fixture +def client(): + app.app_context().push() + app.config["TESTING"] = True + app.config["OIDC_PUBLIC_KEY"] = PUBLIC_KEY + app.config["SECRET_KEY"] = SECRET_KEY + app.config["REDIS_PASSWORD"] = REDIS_PASSWORD + client = app.test_client() + yield client + + +@responses.activate +def test_simple(client): + test_url = app.config["GITLAB_URL"] + "/dummy" + responses.add(responses.GET, test_url, json={"error": "not found"}, status=404) + + resp = requests.get(test_url) + + assert resp.json() == {"error": "not found"} + + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == test_url + assert responses.calls[0].response.text == '{"error": "not found"}' + + +def test_health_endpoint(client): + rv = client.get("/health") + assert b'"Up and running"' in (rv.get_data()) + + +# TODO: currently no endpoint absolutely requires a token +# @responses.activate +# def test_passthrough_notokenflow(client): +# # If a request does not have the required header it should not be let through +# path = urljoin(app.config['SERVICE_PREFIX'], 'v4/projects/') +# rv = client.get(path) +# assert rv.status_code == 401 +# assert b'No authorization header found' in (rv.get_data()) + +# TODO: currently the project mapper is not used, but we keep the other response +# TODO: for future use. + + +def test_gitlab_happyflow(client): + """If a request has the required headers, it should be able to pass through.""" + + def set_dummy_oauth_client(token, key_suffix): + provider_app = OAuthProviderApp(**PROVIDER_APP_DICT) + redis_key = get_redis_key_from_token(access_token, key_suffix=key_suffix) + oauth_client = RenkuWebApplicationClient( + access_token=token, provider_app=provider_app + ) + app.store.set_oauth_client(redis_key, oauth_client) + + access_token = jwt.encode(payload=TOKEN_PAYLOAD, key=PRIVATE_KEY, algorithm="RS256") + headers = {"Authorization": "Bearer {}".format(access_token)} + + app.store = OAuthRedis(FakeStrictRedis(), app.config["SECRET_KEY"]) + + set_dummy_oauth_client(access_token, app.config["CLI_SUFFIX"]) + + set_dummy_oauth_client("some_token", app.config["GL_SUFFIX"]) + + rv = client.get("/?auth=gitlab", headers=headers) + + assert rv.status_code == 200 + assert "Bearer some_token" == rv.headers["Authorization"] diff --git a/chartpress.yaml b/chartpress.yaml index e4603f1a..1d37a314 100644 --- a/chartpress.yaml +++ b/chartpress.yaml @@ -11,4 +11,8 @@ charts: renku-gateway: contextPath: . dockerfilePath: Dockerfile - valuesPath: gateway.image + valuesPath: gateway.image.auth + renku-revproxy: + contextPath: . + dockerfilePath: Dockerfile.revproxy + valuesPath: gateway.reverseProxy.image diff --git a/helm-chart/renku-gateway/values.yaml b/helm-chart/renku-gateway/values.yaml index b90b0d39..ede689d3 100644 --- a/helm-chart/renku-gateway/values.yaml +++ b/helm-chart/renku-gateway/values.yaml @@ -1,4 +1,11 @@ gateway: image: - repository: renku/renku-gateway - tag: "latest" + ## Define the image for the auth middleware + auth: + repository: renku/renku-gateway + tag: "latest" + + reverseProxy: + image: + repository: renku/renku-revproxy + tag: "latest" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..29939036 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1529 @@ +# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[package.dependencies] +typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} + +[[package]] +name = "black" +version = "23.3.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "blinker" +version = "1.6.3" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.7" +files = [ + {file = "blinker-1.6.3-py3-none-any.whl", hash = "sha256:296320d6c28b006eb5e32d4712202dbcdcbf5dc482da298c2f44881c43884aaa"}, + {file = "blinker-1.6.3.tar.gz", hash = "sha256:152090d27c1c5c722ee7e48504b02d76502811ce02e1523553b4cf8c8b3d3a8d"}, +] + +[[package]] +name = "certifi" +version = "2023.11.17" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, +] + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +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." +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.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "chartpress" +version = "2.1.0" +description = "ChartPress: render and publish helm charts and images" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chartpress-2.1.0-py3-none-any.whl", hash = "sha256:988ae1aed0e84ebc561e6b6b2c10464411780445e881784e790e0c90997e95cd"}, + {file = "chartpress-2.1.0.tar.gz", hash = "sha256:aaa905f91b14058107471bc4059b1f6e0390b2cdbfdec418ee3d64a3e2e392dc"}, +] + +[package.dependencies] +docker = ">=3.2.0,<5.0.0 || >5.0.0" +"ruamel.yaml" = ">=0.15.44" + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +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 = "cryptography" +version = "41.0.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, +] + +[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)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "debugpy" +version = "1.7.0" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "debugpy-1.7.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:17ad9a681aca1704c55b9a5edcb495fa8f599e4655c9872b7f9cf3dc25890d48"}, + {file = "debugpy-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1285920a3f9a75f5d1acf59ab1b9da9ae6eb9a05884cd7674f95170c9cafa4de"}, + {file = "debugpy-1.7.0-cp310-cp310-win32.whl", hash = "sha256:a6f43a681c5025db1f1c0568069d1d1bad306a02e7c36144912b26d9c90e4724"}, + {file = "debugpy-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e9571d831ad3c75b5fb6f3efcb71c471cf2a74ba84af6ac1c79ce00683bed4b"}, + {file = "debugpy-1.7.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:538765a41198aa88cc089295b39c7322dd598f9ef1d52eaae12145c63bf9430a"}, + {file = "debugpy-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7e8cf91f8f3f9b5fad844dd88427b85d398bda1e2a0cd65d5a21312fcbc0c6f"}, + {file = "debugpy-1.7.0-cp311-cp311-win32.whl", hash = "sha256:18a69f8e142a716310dd0af6d7db08992aed99e2606108732efde101e7c65e2a"}, + {file = "debugpy-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7515a5ba5ee9bfe956685909c5f28734c1cecd4ee813523363acfe3ca824883a"}, + {file = "debugpy-1.7.0-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:bc8da67ade39d9e75608cdb8601d07e63a4e85966e0572c981f14e2cf42bcdef"}, + {file = "debugpy-1.7.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5036e918c6ba8fc4c4f1fd0207d81db634431a02f0dc2ba51b12fd793c8c9de"}, + {file = "debugpy-1.7.0-cp37-cp37m-win32.whl", hash = "sha256:d5be95b3946a4d7b388e45068c7b75036ac5a610f41014aee6cafcd5506423ad"}, + {file = "debugpy-1.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0e90314a078d4e3f009520c8387aba8f74c3034645daa7a332a3d1bb81335756"}, + {file = "debugpy-1.7.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:1565fd904f9571c430adca597771255cff4f92171486fced6f765dcbdfc8ec8d"}, + {file = "debugpy-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6516f36a2e95b3be27f171f12b641e443863f4ad5255d0fdcea6ae0be29bb912"}, + {file = "debugpy-1.7.0-cp38-cp38-win32.whl", hash = "sha256:2b0e489613bc066051439df04c56777ec184b957d6810cb65f235083aef7a0dc"}, + {file = "debugpy-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:7bf0b4bbd841b2397b6a8de15da9227f1164f6d43ceee971c50194eaed930a9d"}, + {file = "debugpy-1.7.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:ad22e1095b9977af432465c1e09132ba176e18df3834b1efcab1a449346b350b"}, + {file = "debugpy-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f625e427f21423e5874139db529e18cb2966bdfcc1cb87a195538c5b34d163d1"}, + {file = "debugpy-1.7.0-cp39-cp39-win32.whl", hash = "sha256:18bca8429d6632e2d3435055416d2d88f0309cc39709f4f6355c8d412cc61f24"}, + {file = "debugpy-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:dc8a12ac8b97ef3d6973c6679a093138c7c9b03eb685f0e253269a195f651559"}, + {file = "debugpy-1.7.0-py2.py3-none-any.whl", hash = "sha256:f6de2e6f24f62969e0f0ef682d78c98161c4dca29e9fb05df4d2989005005502"}, + {file = "debugpy-1.7.0.zip", hash = "sha256:676911c710e85567b17172db934a71319ed9d995104610ce23fd74a07f66e6f6"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "docker" +version = "6.1.3" +description = "A Python library for the Docker Engine API." +optional = false +python-versions = ">=3.7" +files = [ + {file = "docker-6.1.3-py3-none-any.whl", hash = "sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9"}, + {file = "docker-6.1.3.tar.gz", hash = "sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20"}, +] + +[package.dependencies] +packaging = ">=14.0" +pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} +requests = ">=2.26.0" +urllib3 = ">=1.26.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.3)"] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fakeredis" +version = "2.20.1" +description = "Python implementation of redis API, can be used for testing purposes." +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "fakeredis-2.20.1-py3-none-any.whl", hash = "sha256:d1cb22ed76b574cbf807c2987ea82fc0bd3e7d68a7a1e3331dd202cc39d6b4e5"}, + {file = "fakeredis-2.20.1.tar.gz", hash = "sha256:a2a5ccfcd72dc90435c18cde284f8cdd0cb032eb67d59f3fed907cde1cbffbbd"}, +] + +[package.dependencies] +redis = ">=4" +sortedcontainers = ">=2,<3" + +[package.extras] +bf = ["pybloom-live (>=4.0,<5.0)"] +json = ["jsonpath-ng (>=1.6,<2.0)"] +lua = ["lupa (>=1.14,<3.0)"] + +[[package]] +name = "filelock" +version = "3.12.2" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.7" +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "flake8" +version = "5.0.4" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" + +[[package]] +name = "flask" +version = "2.2.5" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Flask-2.2.5-py3-none-any.whl", hash = "sha256:58107ed83443e86067e41eff4631b058178191a355886f8e479e347fa1285fdf"}, + {file = "Flask-2.2.5.tar.gz", hash = "sha256:edee9b0a7ff26621bd5a8c10ff484ae28737a2410d99b0bb9a6850c7fb977aa0"}, +] + +[package.dependencies] +click = ">=8.0" +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +itsdangerous = ">=2.0" +Jinja2 = ">=3.0" +Werkzeug = ">=2.2.2" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-cors" +version = "4.0.0" +description = "A Flask extension adding a decorator for CORS support" +optional = false +python-versions = "*" +files = [ + {file = "Flask-Cors-4.0.0.tar.gz", hash = "sha256:f268522fcb2f73e2ecdde1ef45e2fd5c71cc48fe03cffb4b441c6d1b40684eb0"}, + {file = "Flask_Cors-4.0.0-py2.py3-none-any.whl", hash = "sha256:bc3492bfd6368d27cfe79c7821df5a8a319e1a6d5eab277a3794be19bdc51783"}, +] + +[package.dependencies] +Flask = ">=0.9" + +[[package]] +name = "flask-kvsession" +version = "0.6.2" +description = "Transparent server-side session support for flask" +optional = false +python-versions = "*" +files = [ + {file = "Flask-KVSession-0.6.2.tar.gz", hash = "sha256:9c0ee93fae089c45baeda0a3fd3ae32a96ee81c34996017749f8b3fd06df936c"}, +] + +[package.dependencies] +Flask = ">=0.8" +itsdangerous = ">=0.20" +simplekv = ">=0.9.2" +six = "*" +werkzeug = "*" + +[[package]] +name = "gunicorn" +version = "21.2.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.5" +files = [ + {file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"}, + {file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"}, +] + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "identify" +version = "2.5.24" +description = "File identification library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, + {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "importlib-metadata" +version = "4.2.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.6" +files = [ + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, +] + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isodate" +version = "0.6.1" +description = "An ISO 8601 date/time/duration parser and formatter" +optional = false +python-versions = "*" +files = [ + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "itsdangerous" +version = "2.1.2" +description = "Safely pass data to untrusted environments and back." +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." +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 = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +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 = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +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.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "nose" +version = "1.3.7" +description = "nose extends unittest to make testing easier" +optional = false +python-versions = "*" +files = [ + {file = "nose-1.3.7-py2-none-any.whl", hash = "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a"}, + {file = "nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac"}, + {file = "nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"}, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pycodestyle" +version = "2.9.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, +] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +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 = "pyflakes" +version = "2.5.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] + +[[package]] +name = "pyjwt" +version = "2.8.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version <= \"3.7\""} + +[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 = "pyparsing" +version = "3.1.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-black" +version = "0.3.12" +description = "A pytest plugin to enable format checking with black" +optional = false +python-versions = ">=2.7" +files = [ + {file = "pytest-black-0.3.12.tar.gz", hash = "sha256:1d339b004f764d6cd0f06e690f6dd748df3d62e6fe1a692d6a5500ac2c5b75a5"}, +] + +[package.dependencies] +black = {version = "*", markers = "python_version >= \"3.6\""} +pytest = ">=3.5.0" +toml = "*" + +[[package]] +name = "pytoml" +version = "0.1.21" +description = "A parser for TOML-0.4.0" +optional = false +python-versions = "*" +files = [ + {file = "pytoml-0.1.21-py2.py3-none-any.whl", hash = "sha256:57a21e6347049f73bfb62011ff34cd72774c031b9828cb628a752225136dfc33"}, + {file = "pytoml-0.1.21.tar.gz", hash = "sha256:8eecf7c8d0adcff3b375b09fe403407aa9b645c499e5ab8cac670ac4a35f61e7"}, +] + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "rdflib" +version = "6.3.2" +description = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information." +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "rdflib-6.3.2-py3-none-any.whl", hash = "sha256:36b4e74a32aa1e4fa7b8719876fb192f19ecd45ff932ea5ebbd2e417a0247e63"}, + {file = "rdflib-6.3.2.tar.gz", hash = "sha256:72af591ff704f4caacea7ecc0c5a9056b8553e0489dd4f35a9bc52dbd41522e0"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.0.0,<5.0.0", markers = "python_version >= \"3.7\" and python_version < \"3.8\""} +isodate = ">=0.6.0,<0.7.0" +pyparsing = ">=2.1.0,<4" + +[package.extras] +berkeleydb = ["berkeleydb (>=18.1.0,<19.0.0)"] +html = ["html5lib (>=1.0,<2.0)"] +lxml = ["lxml (>=4.3.0,<5.0.0)"] +networkx = ["networkx (>=2.0.0,<3.0.0)"] + +[[package]] +name = "redis" +version = "5.0.1" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"}, + {file = "redis-5.0.1.tar.gz", hash = "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} +importlib-metadata = {version = ">=1.0", markers = "python_version < \"3.8\""} +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "responses" +version = "0.23.3" +description = "A utility library for mocking out the `requests` Python library." +optional = false +python-versions = ">=3.7" +files = [ + {file = "responses-0.23.3-py3-none-any.whl", hash = "sha256:e6fbcf5d82172fecc0aa1860fd91e58cbfd96cee5e96da5b63fa6eb3caa10dd3"}, + {file = "responses-0.23.3.tar.gz", hash = "sha256:205029e1cb334c21cb4ec64fc7599be48b859a0fd381a42443cdd600bfe8b16a"}, +] + +[package.dependencies] +pyyaml = "*" +requests = ">=2.30.0,<3.0" +types-PyYAML = "*" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} +urllib3 = ">=1.25.10,<3.0" + +[package.extras] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-requests"] + +[[package]] +name = "ruamel-yaml" +version = "0.18.5" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruamel.yaml-0.18.5-py3-none-any.whl", hash = "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada"}, + {file = "ruamel.yaml-0.18.5.tar.gz", hash = "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.8" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.6" +files = [ + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d92f81886165cb14d7b067ef37e142256f1c6a90a65cd156b063a43da1708cfd"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b5edda50e5e9e15e54a6a8a0070302b00c518a9d32accc2346ad6c984aacd279"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:7048c338b6c86627afb27faecf418768acb6331fc24cfa56c93e8c9780f815fa"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, + {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3fcc54cb0c8b811ff66082de1680b4b14cf8a81dce0d4fbf665c2265a81e07a1"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:665f58bfd29b167039f714c6998178d27ccd83984084c286110ef26b230f259f"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9eb5dee2772b0f704ca2e45b1713e4e5198c18f515b52743576d196348f374d3"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, + {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, +] + +[[package]] +name = "sentry-sdk" +version = "1.39.1" +description = "Python client for Sentry (https://sentry.io)" +optional = false +python-versions = "*" +files = [ + {file = "sentry-sdk-1.39.1.tar.gz", hash = "sha256:320a55cdf9da9097a0bead239c35b7e61f53660ef9878861824fd6d9b2eaf3b5"}, + {file = "sentry_sdk-1.39.1-py2.py3-none-any.whl", hash = "sha256:81b5b9ffdd1a374e9eb0c053b5d2012155db9cbe76393a8585677b753bd5fdc1"}, +] + +[package.dependencies] +blinker = {version = ">=1.1", optional = true, markers = "extra == \"flask\""} +certifi = "*" +flask = {version = ">=0.11", optional = true, markers = "extra == \"flask\""} +markupsafe = {version = "*", optional = true, markers = "extra == \"flask\""} +urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +arq = ["arq (>=0.23)"] +asyncpg = ["asyncpg (>=0.23)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +chalice = ["chalice (>=1.16.0)"] +clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] +grpcio = ["grpcio (>=1.21.1)"] +httpx = ["httpx (>=0.16.0)"] +huey = ["huey (>=2)"] +loguru = ["loguru (>=0.5)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"] +pure-eval = ["asttokens", "executing", "pure-eval"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +tornado = ["tornado (>=5)"] + +[[package]] +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[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-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +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 = "simplekv" +version = "0.14.1" +description = "A key-value storage for binary data, support many backends." +optional = false +python-versions = "*" +files = [ + {file = "simplekv-0.14.1-py2-none-any.whl", hash = "sha256:af91a50af41a286a8b7b93292b21dd1af37f38e9513fea0eb4fa75ce778c1683"}, + {file = "simplekv-0.14.1-py3-none-any.whl", hash = "sha256:fcee8d972d092de0dc83732084e389c9b95839503537ef85c1a2eeb07182f2f5"}, + {file = "simplekv-0.14.1.tar.gz", hash = "sha256:8953a36cb3741ea821c9de1962b5313bf6fe1b927f6ced2a55266eb8ce2cd0f6"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +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 = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "sparqlwrapper" +version = "2.0.0" +description = "SPARQL Endpoint interface to Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SPARQLWrapper-2.0.0-py3-none-any.whl", hash = "sha256:c99a7204fff676ee28e6acef327dc1ff8451c6f7217dcd8d49e8872f324a8a20"}, + {file = "SPARQLWrapper-2.0.0.tar.gz", hash = "sha256:3fed3ebcc77617a4a74d2644b86fd88e0f32e7f7003ac7b2b334c026201731f1"}, +] + +[package.dependencies] +rdflib = ">=6.1.1" + +[package.extras] +dev = ["mypy (>=0.931)", "pandas (>=1.3.5)", "pandas-stubs (>=1.2.0.48)", "setuptools (>=3.7.1)"] +docs = ["sphinx (<5)", "sphinx-rtd-theme"] +keepalive = ["keepalive (>=0.5)"] +pandas = ["pandas (>=1.3.5)"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typed-ast" +version = "1.5.5" +description = "a fork of Python 2 and 3 ast modules with type comment support" +optional = false +python-versions = ">=3.6" +files = [ + {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, + {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, + {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, + {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, + {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"}, + {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"}, + {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"}, + {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"}, + {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"}, + {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"}, + {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, + {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, + {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, + {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, + {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.12" +description = "Typing stubs for PyYAML" +optional = false +python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"}, + {file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"}, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "urllib3" +version = "2.0.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.16.2" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.6" +files = [ + {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, + {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, +] + +[package.dependencies] +distlib = ">=0.3.1,<1" +filelock = ">=3.2,<4" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +platformdirs = ">=2,<3" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] + +[[package]] +name = "websocket-client" +version = "1.6.1" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.7" +files = [ + {file = "websocket-client-1.6.1.tar.gz", hash = "sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd"}, + {file = "websocket_client-1.6.1-py3-none-any.whl", hash = "sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d"}, +] + +[package.extras] +docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "werkzeug" +version = "2.2.3" +description = "The comprehensive WSGI web application library." +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 = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +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.7" +content-hash = "3f397b92fb1dda41d9381ea8fc077e76994c0630b497da9c262ae5f92b065346" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..b39a2660 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[tool.poetry] +name = "renku-gateway" +version = "0.23.1" +description = "" +authors = ["Your Name "] +license = "Apache 2" +packages = [{include = "app"}] + +[tool.poetry.dependencies] +python = "^3.7" +flask = "*" +flask-cors = "*" +flask-kvsession = "*" +gunicorn = "*" +oauthlib = "*" +pyjwt = "^2.8.0" +redis = "*" +requests = "*" +requests-oauthlib = "*" +responses = "*" +sentry-sdk = {extras = ["flask"], version = "*"} +sparqlwrapper = "*" +werkzeug = "*" +cryptography = "*" + +[tool.poetry.dev-dependencies] +pytest = "*" +nose = "*" +responses = "*" +debugpy = "*" +typing-extensions = "*" +chartpress = "*" +pytoml = "*" +black = "*" +pytest-black = "*" +fakeredis = "*" +flake8 = "*" +pre-commit = "*" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..883e79bd --- /dev/null +++ b/setup.cfg @@ -0,0 +1,9 @@ +[tool:pytest] +addopts = --black -v + +[isort] +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +