From 6e88cfb59181be4fd9163646a9d7d8539d964179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Wed, 6 Dec 2023 10:07:55 +0100 Subject: [PATCH 01/15] Adds Kubernetes support policy with integration tests --- .github/workflows/tests.yml | 80 +++++++++++++++++++++++---- README.md | 8 +++ helpers/cleanup | 6 +- helpers/run-in-test-cluster | 15 +++-- helpers/supported-releases | 9 +++ pkg/internal/integration/node_test.go | 8 ++- 6 files changed, 105 insertions(+), 21 deletions(-) create mode 100755 helpers/supported-releases diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0716a75..b0b6fa4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,53 +12,109 @@ on: permissions: contents: read +env: + GO_VERSION: 1.21 + jobs: - Linting: + lint: + name: "Run Linters" runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: - go-version: '1.21' + go-version: '${{ env.GO_VERSION }}' + + - name: Restore cache + uses: actions/cache/restore@v3 + with: + path: | + ~/.cache/golangci-lint + ~/.cache/go-build + key: lint-${{ hashFiles('go.mod') }} - name: Install golangci-lint - run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.53.3 + run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 - name: Install staticcheck - run: go install honnef.co/go/tools/cmd/staticcheck@2023.1.5 + run: go install honnef.co/go/tools/cmd/staticcheck@2023.1.6 - name: Run Linter run: make lint - Units: + - name: Save cache + uses: actions/cache/save@v3 + with: + path: | + ~/.cache/golangci-lint + ~/.cache/go-build + key: lint-${{ hashFiles('go.mod') }} + + unit: + name: "Run Unit Tests" runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: - go-version: '1.21' + go-version: '${{ env.GO_VERSION }}' - name: Run Unit Tests run: make test - Integrations: + supported-releases: + name: "Get Kubernetes Releases" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: "List supported releases" + id: list + run: 'echo "releases=$(helpers/supported-releases)" >> $GITHUB_OUTPUT' + + outputs: + releases: ${{ steps.list.outputs.releases }} + + integration: + name: "Kubernetes ${{ matrix.kubernetes }}" runs-on: ubuntu-latest - needs: Units + + needs: + - lint + - unit + - supported-releases + + strategy: + fail-fast: false + max-parallel: 1 + matrix: + kubernetes: "${{ fromJson(needs.supported-releases.outputs.releases) }}" env: CLOUDSCALE_API_TOKEN: ${{ secrets.CLOUDSCALE_API_TOKEN }} - - # Prevent integration tests from running in parallel. + KUBERNETES: '${{ matrix.kubernetes }}' + + # Prevent integration tests from running in parallel. Ideally this should + # be seuqential, but that won't work due to the following issue: + # + # https://github.com/orgs/community/discussions/5435 + # + # Instead we ensure that only one integration test per supported version + # is run at any given time. concurrency: - group: integration + group: integration-${{ matrix.kubernetes }} steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: - go-version: '1.21' + go-version: '${{ env.GO_VERSION }}' + + - name: Generate cluster prefix + run: echo CLUSTER_PREFIX="k8test-$(uuidgen | cut -d '-' -f 1)" >> $GITHUB_ENV - name: Create Test Cluster run: helpers/run-in-test-cluster diff --git a/README.md b/README.md index 2225c78..ce131a6 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,11 @@ helpers/run-in-test-cluster To clean the cluster up, run `helpers/cleanup`. This is based on [k8test](https://github.com/cloudscale-ch/k8test), our in-house Kubernetes integration test utility. + +## Kubernetes Support Policy + +We aim to support the latest three minor Kubernetes releases. Older releases should work as well, but we do not test them automatically and we may decide not to fix bugs related to older releases. + +For example, at the time of this writing 1.28.4 is the latest release, so we currently support 1.28.x, 1.27.x, and 1.26.x. + +Tests are run reguarly against the latest patch of the previous three minor releases. diff --git a/helpers/cleanup b/helpers/cleanup index badb0de..cacf5de 100755 --- a/helpers/cleanup +++ b/helpers/cleanup @@ -4,8 +4,12 @@ # set -euo pipefail +export CLUSTER_PREFIX="${CLUSTER_PREFIX-k8test}" + if ! test -f k8test/cluster/inventory.yml; then exit 0 fi -k8test/playbooks/destroy-cluster.yml -i k8test/cluster/inventory.yml +k8test/playbooks/destroy-cluster.yml \ + -i k8test/cluster/inventory.yml \ + -e cluster_prefix="$CLUSTER_PREFIX" diff --git a/helpers/run-in-test-cluster b/helpers/run-in-test-cluster index ff3cec7..4a0154a 100755 --- a/helpers/run-in-test-cluster +++ b/helpers/run-in-test-cluster @@ -7,6 +7,7 @@ set -euo pipefail export ANSIBLE_CONFIG="$PWD"/k8test/ansible.cfg export KUBERNETES="${KUBERNETES-latest}" +export CLUSTER_PREFIX="${CLUSTER_PREFIX-k8test}" # Prepares k8test with an existing virtual env, or a newly created on function ensure-k8test() { @@ -73,7 +74,8 @@ function ensure-inventory() { -e control_count=2 \ -e worker_count=2 \ -e kubelet_extra_args='--cloud-provider=external' \ - -e kubernetes="${KUBERNETES}" + -e kubernetes="${KUBERNETES}" \ + -e cluster_prefix="${CLUSTER_PREFIX}" # Those won't really change between runs, so update them during install k8test/playbooks/update-secrets.yml \ @@ -104,7 +106,10 @@ function apply-resources() { kubectl apply -f /tmp/ccm.yml } -ensure-k8test -ensure-inventory -build-container -apply-resources +# Execute if not sourced +if [ "${BASH_SOURCE[0]}" -ef "$0" ]; then + ensure-k8test + ensure-inventory + build-container + apply-resources +fi diff --git a/helpers/supported-releases b/helpers/supported-releases new file mode 100755 index 0000000..ad5f0d2 --- /dev/null +++ b/helpers/supported-releases @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +source helpers/run-in-test-cluster +ensure-k8test > /dev/null + +{ + k8test/helpers/release-set --kubernetes=0 --limit kubernetes | jq -r '.kubernetes.version' + k8test/helpers/release-set --kubernetes=-1 --limit kubernetes | jq -r '.kubernetes.version' + k8test/helpers/release-set --kubernetes=-2 --limit kubernetes | jq -r '.kubernetes.version' +} | jq --raw-input --null-input --compact-output '[inputs]' diff --git a/pkg/internal/integration/node_test.go b/pkg/internal/integration/node_test.go index 10a4884..d65376d 100644 --- a/pkg/internal/integration/node_test.go +++ b/pkg/internal/integration/node_test.go @@ -77,9 +77,9 @@ func (s *IntegrationTestSuite) Servers() []cloudscale.Server { return servers } -func (s *IntegrationTestSuite) ServerNamed(name string) *cloudscale.Server { +func (s *IntegrationTestSuite) FirstServerMatching(name string) *cloudscale.Server { for _, server := range s.Servers() { - if server.Name == name { + if strings.Contains(server.Name, name) { return &server } } @@ -185,7 +185,9 @@ func (s *IntegrationTestSuite) TestNodeRestartServer() { require.Len(s.T(), shutdownNodes(), 0, "no nodes may be shutdown yet") // Shutdown the server - server := s.ServerNamed("k8test-worker-1") + server := s.FirstServerMatching("worker-1") + s.Require().NotNil(server) + err := s.api.Servers.Stop(context.Background(), server.UUID) assert.NoError(s.T(), err, "could not stop server %s", server.Name) From 24e3a2e2f6359f246ba2b7bd552fd4dc80056b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Thu, 7 Dec 2023 11:20:04 +0100 Subject: [PATCH 02/15] Parallelize Kubernetes Integration Test Matrix --- .github/workflows/tests.yml | 18 ++++++------- helpers/run-in-test-cluster | 4 ++- helpers/supported-releases | 9 ------- helpers/test-matrix | 33 ++++++++++++++++++++++++ pkg/internal/integration/main_test.go | 13 +++++++--- pkg/internal/integration/node_test.go | 2 +- pkg/internal/integration/service_test.go | 2 +- 7 files changed, 56 insertions(+), 25 deletions(-) delete mode 100755 helpers/supported-releases create mode 100755 helpers/test-matrix diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b0b6fa4..1f0c263 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,19 +64,19 @@ jobs: - name: Run Unit Tests run: make test - supported-releases: + test-matrix: name: "Get Kubernetes Releases" runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: "List supported releases" + - name: "Generate Test Matrix" id: list - run: 'echo "releases=$(helpers/supported-releases)" >> $GITHUB_OUTPUT' + run: 'echo "tests=$(helpers/test-matrix)" >> $GITHUB_OUTPUT' outputs: - releases: ${{ steps.list.outputs.releases }} + tests: ${{ steps.list.outputs.tests }} integration: name: "Kubernetes ${{ matrix.kubernetes }}" @@ -85,17 +85,18 @@ jobs: needs: - lint - unit - - supported-releases + - test-matrix strategy: fail-fast: false - max-parallel: 1 matrix: - kubernetes: "${{ fromJson(needs.supported-releases.outputs.releases) }}" + include: "${{ fromJson(needs.test-matrix.outputs.tests) }}" env: CLOUDSCALE_API_TOKEN: ${{ secrets.CLOUDSCALE_API_TOKEN }} KUBERNETES: '${{ matrix.kubernetes }}' + SUBNET: '${{ matrix.subnet }}' + CLUSTER_PREFIX: '${{ matrix.cluster_prefix }}' # Prevent integration tests from running in parallel. Ideally this should # be seuqential, but that won't work due to the following issue: @@ -113,9 +114,6 @@ jobs: with: go-version: '${{ env.GO_VERSION }}' - - name: Generate cluster prefix - run: echo CLUSTER_PREFIX="k8test-$(uuidgen | cut -d '-' -f 1)" >> $GITHUB_ENV - - name: Create Test Cluster run: helpers/run-in-test-cluster diff --git a/helpers/run-in-test-cluster b/helpers/run-in-test-cluster index 4a0154a..a80d5c0 100755 --- a/helpers/run-in-test-cluster +++ b/helpers/run-in-test-cluster @@ -8,6 +8,7 @@ set -euo pipefail export ANSIBLE_CONFIG="$PWD"/k8test/ansible.cfg export KUBERNETES="${KUBERNETES-latest}" export CLUSTER_PREFIX="${CLUSTER_PREFIX-k8test}" +export SUBNET="${SUBNET-10.100.1.0/24}" # Prepares k8test with an existing virtual env, or a newly created on function ensure-k8test() { @@ -75,7 +76,8 @@ function ensure-inventory() { -e worker_count=2 \ -e kubelet_extra_args='--cloud-provider=external' \ -e kubernetes="${KUBERNETES}" \ - -e cluster_prefix="${CLUSTER_PREFIX}" + -e cluster_prefix="${CLUSTER_PREFIX}" \ + -e subnet="${SUBNET}" # Those won't really change between runs, so update them during install k8test/playbooks/update-secrets.yml \ diff --git a/helpers/supported-releases b/helpers/supported-releases deleted file mode 100755 index ad5f0d2..0000000 --- a/helpers/supported-releases +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -source helpers/run-in-test-cluster -ensure-k8test > /dev/null - -{ - k8test/helpers/release-set --kubernetes=0 --limit kubernetes | jq -r '.kubernetes.version' - k8test/helpers/release-set --kubernetes=-1 --limit kubernetes | jq -r '.kubernetes.version' - k8test/helpers/release-set --kubernetes=-2 --limit kubernetes | jq -r '.kubernetes.version' -} | jq --raw-input --null-input --compact-output '[inputs]' diff --git a/helpers/test-matrix b/helpers/test-matrix new file mode 100755 index 0000000..213d83f --- /dev/null +++ b/helpers/test-matrix @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Make sure the k8test/helpers/release-set CLI is available +source helpers/run-in-test-cluster +ensure-k8test > /dev/null + +# Returns the matrix entry for the given index (1-n) +function matrixentry { + k8test/helpers/release-set --kubernetes=$(( 1 - $1 )) --limit kubernetes | jq -r '.kubernetes.version' + echo "10.100.$1.0/24" + echo "k8test-$(uuidgen | tr '[:upper:]' '[:lower:]' | cut -d '-' -f 1)" +} + +function matrixobject { + matrixentry "$1" | jq --raw-input --null-input --compact-output '{ + "kubernetes": inputs, + "subnet": inputs, + "cluster_prefix": inputs, + }' +} + +function matrixentries { + matrixobject 1 + matrixobject 2 + matrixobject 3 +} + +function matrixobjects { + matrixentries | jq --null-input --compact-output '[inputs]' +} + +matrixobjects diff --git a/pkg/internal/integration/main_test.go b/pkg/internal/integration/main_test.go index cccc6b1..9df7240 100644 --- a/pkg/internal/integration/main_test.go +++ b/pkg/internal/integration/main_test.go @@ -31,9 +31,10 @@ func TestIntegration(t *testing.T) { type IntegrationTestSuite struct { suite.Suite - k8s kubernetes.Interface - api *cloudscale.Client - ns string + k8s kubernetes.Interface + api *cloudscale.Client + ns string + clusterPrefix string } func (s *IntegrationTestSuite) SetupSuite() { @@ -43,6 +44,12 @@ func (s *IntegrationTestSuite) SetupSuite() { log.Fatalf("could not find K8TEST_PATH environment variable\n") } + if prefix, ok := os.LookupEnv("CLUSTER_PREFIX"); ok { + s.clusterPrefix = prefix + } else { + s.clusterPrefix = "k8test" + } + path := fmt.Sprintf("%s/cluster/admin.conf", k8test) data, err := os.ReadFile(path) if err != nil { diff --git a/pkg/internal/integration/node_test.go b/pkg/internal/integration/node_test.go index d65376d..facf79b 100644 --- a/pkg/internal/integration/node_test.go +++ b/pkg/internal/integration/node_test.go @@ -69,7 +69,7 @@ func (s *IntegrationTestSuite) Servers() []cloudscale.Server { context.Background(), cloudscale.WithTagFilter( cloudscale.TagMap{ - "source": "k8test", + "cluster_prefix": s.clusterPrefix, }, ), ) diff --git a/pkg/internal/integration/service_test.go b/pkg/internal/integration/service_test.go index 2cd2d94..9b0f971 100644 --- a/pkg/internal/integration/service_test.go +++ b/pkg/internal/integration/service_test.go @@ -225,7 +225,7 @@ func (s *IntegrationTestSuite) TestServiceTrafficPolicyLocal() { // Traffic received via "Local" policy has no natting. The address is // going to be private network address of the load balancer. - local_policy_prefix := netip.MustParsePrefix("10.100.10.0/24") + local_policy_prefix := netip.MustParsePrefix("10.100.0.0/16") // Deploy a TCP server that returns the remote IP address. Only use a // single instance as we want to check that the routing works right with From 07a7846c336f623925ba68ef116d7c56633a15e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Thu, 7 Dec 2023 13:23:53 +0100 Subject: [PATCH 03/15] Run integration regularly --- .github/workflows/tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f0c263..987ee0a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,11 +4,17 @@ on: push: branches: - main + pull_request: # Allow to run this workflow manually from the Actions tab workflow_dispatch: + # Run this regularly, to get integration tests results against new + # Kubernetes releases. + schedule: + - cron: '15 15 * * 5' + permissions: contents: read From fd2db71bcc3f16949ac41a1fc427e426ba5e6163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Fri, 8 Dec 2023 13:28:23 +0100 Subject: [PATCH 04/15] Build image separately, then test it This ensures that the image is first built, tested as-is on all controls and uploaded bit-by-bit to the registry. --- .github/workflows/tests.yml | 45 ++++++++++++++++++++++++++++++++ .gitignore | 3 ++- Dockerfile | 2 +- helpers/image-from-ref | 43 +++++++++++++++++++++++++++++++ helpers/run-in-test-cluster | 51 ++++++++++++++++++++++++++++++++----- 5 files changed, 135 insertions(+), 9 deletions(-) create mode 100755 helpers/image-from-ref diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 987ee0a..c204bc6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -84,6 +84,34 @@ jobs: outputs: tests: ${{ steps.list.outputs.tests }} + build-image: + name: "Build Container Image" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Evaluate image name + run: 'helpers/image-from-ref >> $GITHUB_ENV' + + - name: Build image + run: 'docker build --platform=linux/amd64 --tag "$IMAGE" .' + + - name: Export image + run: 'docker image save "$IMAGE" -o image.tar' + + - name: Store hash + run: 'shasum -a 256 image.tar | tee image.tar.sha256' + + - name: Store image + uses: actions/upload-artifact@v4 + with: + name: tested-image + path: | + image.tar + image.tar.sha256 + retention-days: 30d + integration: name: "Kubernetes ${{ matrix.kubernetes }}" runs-on: ubuntu-latest @@ -92,6 +120,7 @@ jobs: - lint - unit - test-matrix + - build-image strategy: fail-fast: false @@ -103,6 +132,7 @@ jobs: KUBERNETES: '${{ matrix.kubernetes }}' SUBNET: '${{ matrix.subnet }}' CLUSTER_PREFIX: '${{ matrix.cluster_prefix }}' + IMAGE_SOURCE: import # Prevent integration tests from running in parallel. Ideally this should # be seuqential, but that won't work due to the following issue: @@ -116,10 +146,25 @@ jobs: steps: - uses: actions/checkout@v3 + + - name: Load image + uses: actions/download-artifact@v4 + with: + name: tested-image + + - name: Validate hash + run: 'shasum --check image.tar.sha256' + - uses: actions/setup-go@v4 with: go-version: '${{ env.GO_VERSION }}' + - name: Evaluate image name + run: 'helpers/image-from-ref >> $GITHUB_ENV' + + - name: Stagger tests + run: 'sleep $((RANDOM % 60 + 1))' + - name: Create Test Cluster run: helpers/run-in-test-cluster diff --git a/.gitignore b/.gitignore index 6528729..a668696 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.iml cover.out k8test -bin/ \ No newline at end of file +bin/ +image.tar diff --git a/Dockerfile b/Dockerfile index e38fae1..29f729b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN apk add --no-cache git WORKDIR /host COPY . /host -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOCACHE=/var/cache/container \ +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOCACHE=/var/cache/image \ go build -trimpath -ldflags "-s -w -X main.version=$VERSION" \ -o bin/cloudscale-cloud-controller-manager \ cmd/cloudscale-cloud-controller-manager/main.go diff --git a/helpers/image-from-ref b/helpers/image-from-ref new file mode 100755 index 0000000..1776907 --- /dev/null +++ b/helpers/image-from-ref @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +""" Looks at the GITHUB_REF environment variable and determins the image name +that should be used for the pipeline. + +For more information: +https://docs.github.com/en/actions/learn-github-actions/contexts#github-context + +The output is meant to be used with $GITHUB_ENV. + +""" + +import argparse +import os +import sys + +REPOSITORY = 'quay.io/cloudscalech/cloudscale-cloud-controller-manager' + +parser = argparse.ArgumentParser(usage=__doc__) +parser.add_argument('--ref', default=os.environ.get('GITHUB_REF')) + + +def main(ref: str) -> int: + if not ref: + print("Either use --ref or set GITHUB_REF") + return 1 + + match ref.split('/'): + case ["refs", "heads", branch]: + tag = f"branch-{branch}" + case ["refs", "pull", pull_request, "merge"]: + tag = f"pull-request-{pull_request}" + case ["refs", "tags", tag_name]: + tag = tag_name + case _: + print(f"'{ref}' did not match any known pattern") + return 1 + + print(f"IMAGE={REPOSITORY}:{tag}") + return 0 + + +if __name__ == '__main__': + sys.exit(main(**vars(parser.parse_args()))) diff --git a/helpers/run-in-test-cluster b/helpers/run-in-test-cluster index a80d5c0..f1e1146 100755 --- a/helpers/run-in-test-cluster +++ b/helpers/run-in-test-cluster @@ -9,6 +9,8 @@ export ANSIBLE_CONFIG="$PWD"/k8test/ansible.cfg export KUBERNETES="${KUBERNETES-latest}" export CLUSTER_PREFIX="${CLUSTER_PREFIX-k8test}" export SUBNET="${SUBNET-10.100.1.0/24}" +export LATEST="quay.io/cloudscalech/cloudscale-cloud-controller-manager:latest" +export IMAGE="${IMAGE-quay.io/cloudscalech/cloudscale-cloud-controller-manager:test}" # Prepares k8test with an existing virtual env, or a newly created on function ensure-k8test() { @@ -85,20 +87,29 @@ function ensure-inventory() { fi } -# Build the latest container each time -function build-container() { - k8test/playbooks/build-container.yml \ +# Build the latest image each time +function build-image() { + k8test/playbooks/build-image.yml \ -i k8test/cluster/inventory.yml \ -e dockerfile=./Dockerfile \ - -e tag=quay.io/cloudscalech/cloudscale-cloud-controller-manager:test \ - -e extra='--build-arg=VERSION=test' + -e tag="$IMAGE" \ + -e extra='--build-arg=VERSION=test' \ + -l controls +} + +# Import an image from the host +function import-image() { + k8test/playbooks/import-image.yml \ + -i k8test/cluster/inventory.yml \ + -e image="$PWD/image.tar" \ + -l controls } # Re-apply the resources each time function apply-resources() { api_url=$(echo "${CLOUDSCALE_API_URL-https://api.cloudscale.ch}" | sed 's|/v1||g') - sed 's/controller-manager:latest/controller-manager:test/g' \ + sed "s|${LATEST}|${IMAGE}|g" \ deploy/kubernetes/releases/latest.yml \ | sed "s|https://api.cloudscale.ch|${api_url}|g" \ > /tmp/ccm.yml @@ -110,8 +121,34 @@ function apply-resources() { # Execute if not sourced if [ "${BASH_SOURCE[0]}" -ef "$0" ]; then + # The image name requires a slash in it, or Podman will add `localhost/` and + # confuse Kubernetes. + if [[ "$IMAGE" != *"/"* ]]; then + echo "\$IMAGE has no slash: $IMAGE" + exit 1 + fi + ensure-k8test ensure-inventory - build-container + + # By default, the image is built every time (with the latest changes + # in the current directory), but it is also possible to import a pre-built + # image (for CI). + # + # The pre-built image is expected to exist as `image.tar` in the + # current directory. It should be created as follows: + # + # export IMAGE=cloudscale-ccm + # export IMAGE_SOURCE=import + # docker build --platform=linux/amd64 -t $IMAGE + # docker save $IMAGE -o image.tar + # run-in-test-cluster + # + if [[ "${IMAGE_SOURCE-build}" == "build" ]]; then + build-image + else + import-image + fi + apply-resources fi From 18dddd22cdf87f7edb349b85ebec4ea18e21570c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Fri, 15 Dec 2023 15:41:20 +0100 Subject: [PATCH 05/15] Fix CCM integration tests on Kubernetes 1.29 --- helpers/run-in-test-cluster | 75 +++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/helpers/run-in-test-cluster b/helpers/run-in-test-cluster index f1e1146..69a84b0 100755 --- a/helpers/run-in-test-cluster +++ b/helpers/run-in-test-cluster @@ -61,7 +61,7 @@ function random-zone() { } # Launches the test cluster, if there's no admin.conf yet -function ensure-inventory() { +function ensure-cluster() { if ! test -d k8test/cluster; then mkdir k8test/cluster fi @@ -71,19 +71,38 @@ function ensure-inventory() { fi if ! test -f k8test/cluster/admin.conf; then + zone="$(random-zone)" + + # First create the cluster, without configuring the network k8test/playbooks/create-cluster.yml \ - -e zone="$(random-zone)" \ + -e zone="$zone" \ -e ssh_key=k8test/cluster/ssh.pub \ -e control_count=2 \ -e worker_count=2 \ -e kubelet_extra_args='--cloud-provider=external' \ -e kubernetes="${KUBERNETES}" \ -e cluster_prefix="${CLUSTER_PREFIX}" \ - -e subnet="${SUBNET}" + -e subnet="${SUBNET}" \ + --tags setup-vms,setup-controls # Those won't really change between runs, so update them during install k8test/playbooks/update-secrets.yml \ -i k8test/cluster/inventory.yml + + # Second, get the CCM running. Without it setting the node IPs, Cilium + # won't work. + update-ccm + + # Finally, setup the cluster to completion + k8test/playbooks/create-cluster.yml \ + -e zone="$zone" \ + -e ssh_key=k8test/cluster/ssh.pub \ + -e control_count=2 \ + -e worker_count=2 \ + -e kubelet_extra_args='--cloud-provider=external' \ + -e kubernetes="${KUBERNETES}" \ + -e cluster_prefix="${CLUSTER_PREFIX}" \ + -e subnet="${SUBNET}" fi } @@ -105,6 +124,30 @@ function import-image() { -l controls } +# Install/update the CCM +function update-ccm() { + # By default, the image is built every time (with the latest changes + # in the current directory), but it is also possible to import a pre-built + # image (for CI). + # + # The pre-built image is expected to exist as `image.tar` in the + # current directory. It should be created as follows: + # + # export IMAGE=cloudscale-ccm + # export IMAGE_SOURCE=import + # docker build --platform=linux/amd64 -t $IMAGE + # docker save $IMAGE -o image.tar + # run-in-test-cluster + # + if [[ "${IMAGE_SOURCE-build}" == "build" ]]; then + build-image + else + import-image + fi + + apply-resources +} + # Re-apply the resources each time function apply-resources() { api_url=$(echo "${CLOUDSCALE_API_URL-https://api.cloudscale.ch}" | sed 's|/v1||g') @@ -121,6 +164,7 @@ function apply-resources() { # Execute if not sourced if [ "${BASH_SOURCE[0]}" -ef "$0" ]; then + # The image name requires a slash in it, or Podman will add `localhost/` and # confuse Kubernetes. if [[ "$IMAGE" != *"/"* ]]; then @@ -128,27 +172,12 @@ if [ "${BASH_SOURCE[0]}" -ef "$0" ]; then exit 1 fi + first_run=$(test ! -f k8test/cluster/inventory.yml && echo "yes" || echo "no") + ensure-k8test - ensure-inventory + ensure-cluster - # By default, the image is built every time (with the latest changes - # in the current directory), but it is also possible to import a pre-built - # image (for CI). - # - # The pre-built image is expected to exist as `image.tar` in the - # current directory. It should be created as follows: - # - # export IMAGE=cloudscale-ccm - # export IMAGE_SOURCE=import - # docker build --platform=linux/amd64 -t $IMAGE - # docker save $IMAGE -o image.tar - # run-in-test-cluster - # - if [[ "${IMAGE_SOURCE-build}" == "build" ]]; then - build-image - else - import-image + if [[ "$first_run" == "no" ]]; then + update-ccm fi - - apply-resources fi From a7b866275a64559548015a87daa3c0dba679e3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Tue, 19 Dec 2023 10:48:14 +0100 Subject: [PATCH 06/15] Add release process automation --- .github/workflows/tests.yml | 4 + README.md | 184 +++- deploy/{kubernetes/releases => }/latest.yml | 0 helpers/release | 483 +++++++++ helpers/run-in-test-cluster | 2 +- poetry.lock | 1016 +++++++++++++++++++ pyproject.toml | 33 + 7 files changed, 1711 insertions(+), 11 deletions(-) rename deploy/{kubernetes/releases => }/latest.yml (100%) create mode 100755 helpers/release create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c204bc6..b52a4ed 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -174,6 +174,10 @@ jobs: - name: Run Integration Tests run: make integration + - name: Wait For Kubernetes-Internal Cleanup + if: always() + run: sleep 30 + - name: Destroy Test Cluster if: always() run: helpers/cleanup diff --git a/README.md b/README.md index ce131a6..4558572 100644 --- a/README.md +++ b/README.md @@ -4,32 +4,196 @@ Integrate your Kubernetes cluster with cloudscale.ch infrastructure, with our cloud controller manager (CCM). -Provides the following features: +- Automatically provisions load balancers for [`LoadBalancer`](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) services. +- Enriches `Node` metadata with information from our cloud. +- Updates `Node` state depending on the state of the underlying VM. - Automatically provisions load balancers for [`LoadBalancer` services](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer). - Enriches `Node` metadata with information from our cloud. - Updates `Node` state depending on the state of the underlying VM. -## Test Cluster +## Kubernetes Support Policy + +We support the three latest minor Kubernetes releases. + +For example, if the current release is `1.29.0`, we support the following: + +- `1.29.x` +- `1.28.x` +- `1.27.x` + +Older releases should work as well, but we do not test them automatically and we may decide not to fix bugs related to older releases. + +## Try It Out To test the CCM on a vanilla Kubernetes cluster, you can use `helpers/run-in-test-cluster`. This will create a small Kubernetes cluster at cloudscale.ch, and install the current development version in it. -Note that you need a `CLOUDSCALE_API_TOKEN` for this to work, and this may incur costs on your side: - ```bash export CLOUDSCALE_API_TOKEN="..." helpers/run-in-test-cluster ``` -To clean the cluster up, run `helpers/cleanup`. +You can access the created cluster as follows: -This is based on [k8test](https://github.com/cloudscale-ch/k8test), our in-house Kubernetes integration test utility. +```bash +# Via kubectl +export KUBECONFIG = k8test/cluster/admin.conf +kubectl get nodes -## Kubernetes Support Policy +# Via ssh +ssh ubuntu@ -i k8test/cluster/ssh +``` + +To cleanup: + +```bash +helpers/cleanup +``` + +> :warning: This may incur costs on your side. Clusters that are removed may also leave behind loadbalancers, if associated services are not removed first. Please look at https://control.cloudscale.ch after cleanup to ensure that everything was removed. + +# Operator Manual + +## Installation + +### Configuring the Cluster + +To install the CCM on a new cluster, you need to configure your `kubelet` to always use the following argument: -We aim to support the latest three minor Kubernetes releases. Older releases should work as well, but we do not test them automatically and we may decide not to fix bugs related to older releases. +```bash +kubelet --cloud-provider=external +``` + +This should be persisted indefinitely, depending on your distribution. Feel free to open an issue if you have trouble locating the right place to do this in your setup. + +A cluster careted this way **will start all nodes tainted** as follows: + +```yaml +node.cloudprovider.kubernetes.io/uninitialized: true +``` + +This taint will be removed, once the CCM has initalized the nodes. + +### Node IPs + +With Kubernetes 1.29 and above, the nodes do not gain a node IP in Kubernetes, until the CCM has run. + +This can be problematic for certain network plugins like Cilium, which expect this to exist. You may have to install such plugins after the CCM, or wait for them to heal after the CCM has been installed. + +Alternatively you can configure `--node-ips` with `kubectl`, to explicitly set the IPs, but this may cause problems if the IPs set via `kubectl` differ from the IPs determined by the CCM. + +See https://github.com/kubernetes/kubernetes/pull/121028 + +> :bulb: We recommend installing the CCM before installing the network plugin. + +### Storing the API Token + +To configure the CCM, the following secret needs to be configured: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: cloudscale + namespace: kube-system +stringData: + access-token: "..." +``` + +Create a file like this named `cloudscale-api-token.yml`, with your token filled in, and run the following: + +```bash +kubectl apply -f cloudscale-api-token.yml +``` -For example, at the time of this writing 1.28.4 is the latest release, so we currently support 1.28.x, 1.27.x, and 1.26.x. +You can get a token on https://control.cloudscale.ch. Be aware that you need a read/write token. The token should not be deleted while it is in use, so we recommend naming the token accordingly. -Tests are run reguarly against the latest patch of the previous three minor releases. +### Installing the CCM + +To install the CCM, run the following command. This can be done as soon as the control-plane is reachable and the secret has been configured. The CCM will be installed on all control nodes, even if they are uninitialized: + +To install the latest version: + +``` +kubectl apply -f https://github.com/cloudscale-ch/cloudscale-cloud-controller-manager/releases/latest/download/config.yml +``` + +To install a specific version, or to upgrade to a new version, have a look at the [list of releases](https://github.com/cloudscale-ch/cloudscale-cloud-controller-manager/releases]. + +Each release has a version-specific `kubectl apply` command in its release description. + +### Existing Clusters + +For existing clusters we recommend the following installation order: + +1. [Storing the API Token](#storing-the-api-token) +2. [Installing the CCM](#installing-the-ccm) +3. [Configuring the Cluster](#configuring-the-cluster) + +For step three you need to restart the kubelet once on each node (serially). + +You can verify that the CCM is running, by having a look at the status of the `cloudscale-cloud-controller-manager` daemonset and its log. + +At this point, `LoadBalancer` service resources can already be used, but the Node metadata will only be updated on the nodes once they have been tainted briefly as follows: + +```bash +kubectl taint node node.cloudprovider.kubernetes.io/uninitialized=true:NoSchedule +``` + +This taint should be immediately removed by the CCM and the metadata provided by the CCM should be added to the labels and addresses of the node. + +You should also find a `ProviderID` spec on each node. + +> :warning: These instructions may not be right for your cluster, so be sure to test this in a staging environment. + +# Developer Manual + +## Releases + +Relases are not integrated into the CI process. This remains a manual step, as releases via tagging via GitHub tend to be finicky and hard to control precisely. +Instead there is a release CLI, which ensures that a release is tested, before uploading the tested container image to Quay.io and publishing a new release. + +There are two ways to create a release: + +1. From a separate branch (must be a pre-release). +2. From the main branch (for the real release). + +To create releases, you need to install some Python dependencies (using Python 3.11+): + +```bash +python3 -m venv venv +source venv/bin/activate + +pip install poetry +poetry install +``` + +You will also need to set the following environment variables: + +**`GITHUB_TOKEN`** + +A fine-grained GitHub access token with the following properties: + +- Limited to this repository. +- Actions: Read-Only. +- Commit statuses: Read-Only. +- Contents: Read and Write. + +**`QUAY_BOT_USER` / `QUAY_BOT_PASS`** + +Quay bot user with permission to write to the `cloudscalech/cloudscale-cloud-controller-manager` repository on quay.io. + +You can then use `helpers/release create` to create a new release: + +```bash +export GITHUB_TOKEN="github_pat_..." +export QUAY_BOT_USER="..." +export QUAY_BOT_PASS="..." + +# Create a new minor release of the main branch +helpers/release create minor + +# Create a new minor pre-release for a test branch +helpers/relese create minor --pre --ref test/branch +``` diff --git a/deploy/kubernetes/releases/latest.yml b/deploy/latest.yml similarity index 100% rename from deploy/kubernetes/releases/latest.yml rename to deploy/latest.yml diff --git a/helpers/release b/helpers/release new file mode 100755 index 0000000..8aa1fb8 --- /dev/null +++ b/helpers/release @@ -0,0 +1,483 @@ +#!/usr/bin/env python3 +import click +import requests +import subprocess +import sys +import tempfile +import textwrap + +from enum import Enum +from functools import cached_property +from github import Github +from github.Artifact import Artifact +from github.Commit import Commit +from github.GitRelease import GitRelease +from github.GithubException import UnknownObjectException +from github.Repository import Repository +from github.WorkflowRun import WorkflowRun +from io import BytesIO +from pathlib import Path +from pydantic import BaseModel +from semver import Version +from sys import stderr +from typer import Argument +from typer import Option +from typer import Typer +from typing import Annotated +from typing import IO +from typing import Iterator +from typing import Optional +from typing import Tuple +from zipfile import ZipFile + + +# The source repository on GitHub +GIT_REPO = 'cloudscale-ch/cloudscale-cloud-controller-manager' + +# The container repository on Quay.io +IMAGE_REPO = 'cloudscalech/cloudscale-cloud-controller-manager' + + +cli = Typer(add_completion=False) + + +class Increment(str, Enum): + """ The kinds of version bumps that are supported. """ + + major = "major" + minor = "minor" + patch = "patch" + + +class ReleaseError(Exception): + def __init__(self, message: str) -> None: + super().__init__() + self.message = message + + +class FlightCheckError(BaseModel): + + # Whether the error can be overruled using '--force' + skippable: bool + + # The description of the error + message: str + + +class ReleaseAPI(BaseModel): + """ Wraps access to GitHub and Quay.io with helper methods and caches. """ + + # A fine-grained GitHub access token with the following properties: + # - Limited to the GIT_REPO repository. + # - Actions: Read-Only. + # - Commit statuses: Read-Only. + # - Contents: Read and Write. + github_token: str + + # A quay bot username/token with write access to IMAGE_REPO. + quay_auth: Tuple[str, str] + + @cached_property + def api(self) -> Github: + return Github(self.github_token) + + @cached_property + def repository(self) -> Repository: + return self.api.get_repo(GIT_REPO) + + @cached_property + def versions(self) -> list[Version]: + versions = [] + + for release in self.repository.get_releases(): + try: + versions.append(Version.parse(release.tag_name)) + except ValueError: + print(f"[Warning] Not semver: {release.html_url}", file=stderr) + + versions.sort() + return versions + + @cached_property + def image_tags(self) -> list[str]: + r = requests.get(f'https://quay.io/api/v1/repository/{IMAGE_REPO}/tag') + r.raise_for_status() + + return [t['name'] for t in r.json()['tags'] if 'end_ts' not in t] + + def resolve_ref(self, ref: str) -> Commit: + return self.repository.get_commit(ref) + + def has_release(self, commit: Commit) -> bool: + for release in self.repository.get_releases(): + if release.target_commitish == commit.sha: + return True + + return False + + def ensure_latest(self) -> None: + releases = list(self.repository.get_releases()) + latest = None + latest_version = None + + for release in releases: + if release.prerelease: + continue + + try: + version = Version.parse(release.tag_name) + except ValueError: + continue + + if latest is None or version > latest_version: + latest = release + latest_version = version + continue + + if not latest: + return + + latest._requester.requestJsonAndCheck( + "PATCH", latest.url, input={'make_latest': 'true'}) + + def workflow_run(self, commit: Commit) -> WorkflowRun | None: + for run in self.repository.get_workflow_runs(head_sha=commit.sha): + return run + + return None + + def workflow_image_artifact(self, run: WorkflowRun) -> Artifact | None: + for artifact in run.get_artifacts(): + if artifact.name == 'tested-image': + return artifact + + return None + + def next_version(self, old: None | str, inc: Increment, pre: bool) \ + -> Version: + + match old, self.versions: + case None, []: + old_version = Version(0, 0, 0) + case None, versions: + old_version = versions[-1] + case old, _ if old is not None: + try: + old_version = Version.parse(old) + except ValueError: + raise ReleaseError(f"Not a semantic version: '{old}'") + case _: + raise ReleaseError(f"No next version for {old}") + + if pre and old_version.prerelease: + return old_version.bump_prerelease() + + if not pre and old_version.prerelease: + return old_version.finalize_version() + + match inc: + case Increment.major: + new_version = old_version.bump_major() + case Increment.minor: + new_version = old_version.bump_minor() + case Increment.patch: + new_version = old_version.bump_patch() + + if pre: + new_version = new_version.bump_prerelease() + + return new_version + + def download(self, url: str, output: IO[bytes]) -> None: + """ Download the given URL using the GitHub auth token. """ + + assert url.startswith('https://api.github.com') + headers = {'Authorization': f'token {self.github_token}'} + + with requests.get(url, stream=True, headers=headers) as response: + response.raise_for_status() + + for chunk in response.iter_content(chunk_size=8192): + output.write(chunk) + + def docker_login(self) -> str: + """ Login to quay.io using Docker. + + Returns a non-empty string on error, an empty string on success. + + """ + + result = subprocess.run( + ( + 'docker', 'login', 'quay.io', + '--username', self.quay_auth[0], + '--password-stdin' + ), + input=self.quay_auth[1].encode('utf-8') + b"\n", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + if result.returncode != 0: + return result.stderr.decode('utf-8') + + return "" + + def release_config(self, version: Version) -> str: + config = Path('deploy/latest.yml').read_text() + return config.replace(':latest', f':{version}') + + def release_message(self, version: Version) -> str: + repo_url = f'https://github.com/{GIT_REPO}' + readme = f'{repo_url}/blob/{version}/README.md' + + return textwrap.dedent(f"""\ + ## Configuration + + This release can be installed with the following command: + + ``` + kubectl apply -f {repo_url}/releases/download/{version}/config.yml + ``` + + Please refer to the [README]({readme}) for detailed instructions. + """) + + def preflight_check(self, version: Version, commit: Commit) \ + -> Iterator[FlightCheckError]: + """ Ensures the given version can be released. + + If not, at least one FlightCheckError is yielded. + + """ + + # Check if version already exists + if version in self.versions: + yield FlightCheckError( + skippable=True, + message=f"{version} already exists as GitHub release") + + # Check if image already exists + if str(version) in self.image_tags: + yield FlightCheckError( + skippable=True, + message=f"{version} already exists as container image") + + # Check if commit already has a release + if self.has_release(commit): + yield FlightCheckError( + skippable=True, + message=f"{commit.sha} already has a release") + + # Ensure workflow is successful + if run := self.workflow_run(commit): + if run.conclusion is None: + yield FlightCheckError( + skippable=True, + message=f"{run.url} is still pending") + + elif run.conclusion != 'success': + yield FlightCheckError( + skippable=True, + message=f"{run.url} unsuccessful: {run.conclusion}") + + else: + yield FlightCheckError( + skippable=False, + message=f"No run for {commit.sha}") + + # Ensure workflow has an image + if run and not self.workflow_image_artifact(run): + yield FlightCheckError( + skippable=False, + message=f"{run.url} has no tested-image artifact") + + # Validate we can login to quay.io + if err := self.docker_login(): + yield FlightCheckError( + skippable=False, + message=f"Could not login to quay.io: {err}") + + # Ensure non-prereleases are in the main branch + if not version.prerelease: + for c in self.repository.get_commits(sha='main'): + if c == commit: + break + else: + yield FlightCheckError( + skippable=False, + message=f"{commit.sha} is not part of the main branch") + + def create(self, commit: Commit, version: Version, force: bool) \ + -> GitRelease: + """ Creates the release. Assumes pre-flight checks were run. """ + + image = f"quay.io/{IMAGE_REPO}:{version}" + + run = self.workflow_run(commit) + if not run: + raise ReleaseError(f"No run found for {commit.sha}") + + artifact = self.workflow_image_artifact(run) + if not artifact: + raise ReleaseError(f"No artifact found for {run.url}") + + with tempfile.TemporaryDirectory() as tempdir: + zip = Path(tempdir) / 'image.zip' + + with zip.open('wb') as f: + self.download(artifact.archive_download_url, f) + + with ZipFile(zip, 'r') as z: + z.extractall(tempdir) + + subprocess.check_output( + ('shasum', '--check', 'image.tar.sha256'), cwd=tempdir) + + output = subprocess.check_output( + ('docker', 'load', '-i', 'image.tar')).decode('utf-8') + + for line in output.splitlines(): + if line.startswith('Loaded image: '): + imported_image = line.removeprefix('Loaded image: ') + break + else: + raise ReleaseError( + f'Expected "Loaded image: " in output:\n {output}') + + if err := self.docker_login(): + raise ReleaseError(f'Docker login failed: {err}') + + subprocess.check_output(('docker', 'tag', imported_image, image)) + subprocess.check_output(('docker', 'push', image)) + + if not version.prerelease: + versions = [v for v in self.versions if not v.prerelease] + versions.sort() + + if self.versions[-1] <= version: + tag = image.replace(f':{version}', ':latest') + subprocess.check_output(('docker', 'tag', imported_image, tag)) + subprocess.check_output(('docker', 'push', tag)) + + # Get the release before deleting the tag, or we won't find it + try: + existing_release = self.repository.get_release(str(version)) + except UnknownObjectException: + existing_release = None + + # Delete the tag without looking (ignore if it does not exist) + if force: + subprocess.run( + ('git', 'tag', '-d', str(version)), + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + subprocess.run( + ('git', 'push', 'origin', '--delete', str(version)), + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + subprocess.check_output(( + 'git', 'tag', '-a', str(version), + '-m', f"Release {version}", + commit.sha), stderr=subprocess.STDOUT) + + subprocess.check_output( + ('git', 'push', '--tags'), stderr=subprocess.STDOUT) + + if existing_release and not force: + raise ReleaseError(f"A release for {version} already exists") + elif existing_release and force: + existing_release.delete_release() + + release = self.repository.create_git_release( + tag=str(version), + name=str(version), + message=self.release_message(version), + generate_release_notes=True, + target_commitish=commit.sha, + prerelease=False, + ) + + self.ensure_latest() + + config = self.release_config(version).encode('utf-8') + + release.upload_asset_from_memory( + file_like=BytesIO(config), + file_size=len(config), + name='config.yml', + content_type='text/yaml', + ) + + return release + + +@cli.command(name='create') +def create_command( + increment: Annotated[Increment, Argument()], + quay_bot_user: Annotated[str, Option(envvar='QUAY_BOT_USER')], + quay_bot_pass: Annotated[str, Option(envvar='QUAY_BOT_PASS')], + github_token: Annotated[str, Option(envvar='GITHUB_TOKEN')], + pre: Annotated[bool, Option()] = False, + ref: Annotated[str, Option()] = 'main', + force: Annotated[bool, Option()] = False, + old_version: Annotated[Optional[str], Option()] = None, +) -> None: + """ Create a new release. """ + + api = ReleaseAPI( + github_token=github_token, + quay_auth=(quay_bot_user, quay_bot_pass), + ) + + try: + print("Running preflight check") + new_version = api.next_version(old_version, increment, pre) + + commit = api.resolve_ref(ref) + print(f"Commit: {commit.sha}") + + errors = tuple(api.preflight_check(new_version, commit)) + fatal = sum(1 for e in errors if not e.skippable) + + print('') + + if errors: + for err in errors: + if not err.skippable: + print("FATAL:", err.message) + else: + print("ERROR:", err.message) + else: + print("Found no errors ✓") + + if errors and not force: + sys.exit(1) + + if fatal: + sys.exit(1) + + print('') + + if not click.confirm(f"Do you want to release {new_version}?"): + sys.exit(1) + + print('') + + except ReleaseError as e: + print('ERROR:', e.message) + sys.exit(1) + + release = api.create(commit, new_version, force) + print("Success:", release.html_url) + + +@cli.command(name='list', hidden=True) +def list_command() -> None: + """ List existing releases """ + + raise NotImplementedError() + + +if __name__ == '__main__': + cli() diff --git a/helpers/run-in-test-cluster b/helpers/run-in-test-cluster index 69a84b0..73c6139 100755 --- a/helpers/run-in-test-cluster +++ b/helpers/run-in-test-cluster @@ -153,7 +153,7 @@ function apply-resources() { api_url=$(echo "${CLOUDSCALE_API_URL-https://api.cloudscale.ch}" | sed 's|/v1||g') sed "s|${LATEST}|${IMAGE}|g" \ - deploy/kubernetes/releases/latest.yml \ + deploy/latest.yml \ | sed "s|https://api.cloudscale.ch|${api_url}|g" \ > /tmp/ccm.yml diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..a8bd86d --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1016 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[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.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[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 = "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\""} + +[[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 = "deprecated" +version = "1.2.14" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] + +[[package]] +name = "docstring-to-markdown" +version = "0.13" +description = "On the fly conversion of Python docstrings to markdown" +optional = false +python-versions = ">=3.6" +files = [ + {file = "docstring-to-markdown-0.13.tar.gz", hash = "sha256:3025c428638ececae920d6d26054546a20335af3504a145327e657e7ad7ce1ce"}, + {file = "docstring_to_markdown-0.13-py3-none-any.whl", hash = "sha256:aa487059d0883e70e54da25c7b230e918d9e4d40f23d6dfaa2b73e4225b2d7dd"}, +] + +[[package]] +name = "flake8" +version = "6.1.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" + +[[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 = "jedi" +version = "0.19.1" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[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" +version = "1.7.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[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 = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[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 = "pydantic" +version = "2.5.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, + {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.14.5" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.14.5" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, + {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, + {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, + {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, + {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, + {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, + {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, + {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, + {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, + {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, + {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, + {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, + {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, + {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, + {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, + {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, + {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, + {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, + {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, + {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325"}, + {file = "pydantic_core-2.14.5-cp37-none-win32.whl", hash = "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405"}, + {file = "pydantic_core-2.14.5-cp37-none-win_amd64.whl", hash = "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588"}, + {file = "pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, + {file = "pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, + {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, + {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, + {file = "pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, + {file = "pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, + {file = "pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, + {file = "pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, + {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, + {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, + {file = "pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, + {file = "pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, + {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pyflakes" +version = "3.1.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, +] + +[[package]] +name = "pygithub" +version = "2.1.1" +description = "Use the full Github API v3" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyGithub-2.1.1-py3-none-any.whl", hash = "sha256:4b528d5d6f35e991ea5fd3f942f58748f24938805cb7fcf24486546637917337"}, + {file = "PyGithub-2.1.1.tar.gz", hash = "sha256:ecf12c2809c44147bce63b047b3d2e9dac8a41b63e90fcb263c703f64936b97c"}, +] + +[package.dependencies] +Deprecated = "*" +pyjwt = {version = ">=2.4.0", extras = ["crypto"]} +pynacl = ">=1.4.0" +python-dateutil = "*" +requests = ">=2.14.0" +typing-extensions = ">=4.0.0" +urllib3 = ">=1.26.0" + +[[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] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + +[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 = "pylsp-mypy" +version = "0.6.8" +description = "Mypy linter for the Python LSP Server" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pylsp-mypy-0.6.8.tar.gz", hash = "sha256:3f8307ca07d7e253e50e38c5fe31c371ceace0bc33d31c3429fa035d6d41bd5f"}, + {file = "pylsp_mypy-0.6.8-py3-none-any.whl", hash = "sha256:3ea7c406d0f100317a212d8cd39075a2c139f1a4a2866d4412fe531b3f23b381"}, +] + +[package.dependencies] +mypy = ">=0.981" +python-lsp-server = ">=1.7.0" +tomli = ">=1.1.0" + +[package.extras] +test = ["coverage", "pytest", "pytest-cov", "tox"] + +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-lsp-jsonrpc" +version = "1.1.2" +description = "JSON RPC 2.0 server library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-lsp-jsonrpc-1.1.2.tar.gz", hash = "sha256:4688e453eef55cd952bff762c705cedefa12055c0aec17a06f595bcc002cc912"}, + {file = "python_lsp_jsonrpc-1.1.2-py3-none-any.whl", hash = "sha256:7339c2e9630ae98903fdaea1ace8c47fba0484983794d6aafd0bd8989be2b03c"}, +] + +[package.dependencies] +ujson = ">=3.0.0" + +[package.extras] +test = ["coverage", "pycodestyle", "pyflakes", "pylint", "pytest", "pytest-cov"] + +[[package]] +name = "python-lsp-server" +version = "1.9.0" +description = "Python Language Server for the Language Server Protocol" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-lsp-server-1.9.0.tar.gz", hash = "sha256:dc0c8298f0222fd66a52aa3170f3a5c8fe3021007a02098bb72f7fd8df353d13"}, + {file = "python_lsp_server-1.9.0-py3-none-any.whl", hash = "sha256:6b947cf9dc33d7bed9abc936bb173140fcf606b6eb50cf02e27d4cb09f10d3fb"}, +] + +[package.dependencies] +docstring-to-markdown = "*" +flake8 = {version = ">=6.1.0,<7", optional = true, markers = "extra == \"flake8\""} +jedi = ">=0.17.2,<0.20.0" +pluggy = ">=1.0.0" +python-lsp-jsonrpc = ">=1.1.0,<2.0.0" +ujson = ">=3.0.0" + +[package.extras] +all = ["autopep8 (>=2.0.4,<2.1.0)", "flake8 (>=6.1.0,<7)", "mccabe (>=0.7.0,<0.8.0)", "pycodestyle (>=2.11.0,<2.12.0)", "pydocstyle (>=6.3.0,<6.4.0)", "pyflakes (>=3.1.0,<3.2.0)", "pylint (>=2.5.0,<3.1)", "rope (>1.2.0)", "whatthepatch (>=1.0.2,<2.0.0)", "yapf (>=0.33.0)"] +autopep8 = ["autopep8 (>=1.6.0,<2.1.0)"] +flake8 = ["flake8 (>=6.1.0,<7)"] +mccabe = ["mccabe (>=0.7.0,<0.8.0)"] +pycodestyle = ["pycodestyle (>=2.11.0,<2.12.0)"] +pydocstyle = ["pydocstyle (>=6.3.0,<6.4.0)"] +pyflakes = ["pyflakes (>=3.1.0,<3.2.0)"] +pylint = ["pylint (>=2.5.0,<3.1)"] +rope = ["rope (>1.2.0)"] +test = ["coverage", "flaky", "matplotlib", "numpy", "pandas", "pylint (>=2.5.0,<3.1)", "pyqt5", "pytest", "pytest-cov"] +websockets = ["websockets (>=10.3)"] +yapf = ["whatthepatch (>=1.0.2,<2.0.0)", "yapf (>=0.33.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 = "semver" +version = "3.0.2" +description = "Python helper for Semantic Versioning (https://semver.org)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, + {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, +] + +[[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 = "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 = "typer" +version = "0.9.0" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.6" +files = [ + {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"}, + {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"}, +] + +[package.dependencies] +click = ">=7.1.1,<9.0.0" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] +test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] + +[[package]] +name = "types-requests" +version = "2.31.0.10" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.7" +files = [ + {file = "types-requests-2.31.0.10.tar.gz", hash = "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92"}, + {file = "types_requests-2.31.0.10-py3-none-any.whl", hash = "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc"}, +] + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "ujson" +version = "5.8.0" +description = "Ultra fast JSON encoder and decoder for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ujson-5.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4511560d75b15ecb367eef561554959b9d49b6ec3b8d5634212f9fed74a6df1"}, + {file = "ujson-5.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9399eaa5d1931a0ead49dce3ffacbea63f3177978588b956036bfe53cdf6af75"}, + {file = "ujson-5.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4e7bb7eba0e1963f8b768f9c458ecb193e5bf6977090182e2b4f4408f35ac76"}, + {file = "ujson-5.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40931d7c08c4ce99adc4b409ddb1bbb01635a950e81239c2382cfe24251b127a"}, + {file = "ujson-5.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d53039d39de65360e924b511c7ca1a67b0975c34c015dd468fca492b11caa8f7"}, + {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bdf04c6af3852161be9613e458a1fb67327910391de8ffedb8332e60800147a2"}, + {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a70f776bda2e5072a086c02792c7863ba5833d565189e09fabbd04c8b4c3abba"}, + {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f26629ac531d712f93192c233a74888bc8b8212558bd7d04c349125f10199fcf"}, + {file = "ujson-5.8.0-cp310-cp310-win32.whl", hash = "sha256:7ecc33b107ae88405aebdb8d82c13d6944be2331ebb04399134c03171509371a"}, + {file = "ujson-5.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:3b27a8da7a080add559a3b73ec9ebd52e82cc4419f7c6fb7266e62439a055ed0"}, + {file = "ujson-5.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:193349a998cd821483a25f5df30b44e8f495423840ee11b3b28df092ddfd0f7f"}, + {file = "ujson-5.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ddeabbc78b2aed531f167d1e70387b151900bc856d61e9325fcdfefb2a51ad8"}, + {file = "ujson-5.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ce24909a9c25062e60653073dd6d5e6ec9d6ad7ed6e0069450d5b673c854405"}, + {file = "ujson-5.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27a2a3c7620ebe43641e926a1062bc04e92dbe90d3501687957d71b4bdddaec4"}, + {file = "ujson-5.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b852bdf920fe9f84e2a2c210cc45f1b64f763b4f7d01468b33f7791698e455e"}, + {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:20768961a6a706170497129960762ded9c89fb1c10db2989c56956b162e2a8a3"}, + {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e0147d41e9fb5cd174207c4a2895c5e24813204499fd0839951d4c8784a23bf5"}, + {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e3673053b036fd161ae7a5a33358ccae6793ee89fd499000204676baafd7b3aa"}, + {file = "ujson-5.8.0-cp311-cp311-win32.whl", hash = "sha256:a89cf3cd8bf33a37600431b7024a7ccf499db25f9f0b332947fbc79043aad879"}, + {file = "ujson-5.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3659deec9ab9eb19e8646932bfe6fe22730757c4addbe9d7d5544e879dc1b721"}, + {file = "ujson-5.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:102bf31c56f59538cccdfec45649780ae00657e86247c07edac434cb14d5388c"}, + {file = "ujson-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:299a312c3e85edee1178cb6453645217ba23b4e3186412677fa48e9a7f986de6"}, + {file = "ujson-5.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e385a7679b9088d7bc43a64811a7713cc7c33d032d020f757c54e7d41931ae"}, + {file = "ujson-5.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad24ec130855d4430a682c7a60ca0bc158f8253ec81feed4073801f6b6cb681b"}, + {file = "ujson-5.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16fde596d5e45bdf0d7de615346a102510ac8c405098e5595625015b0d4b5296"}, + {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6d230d870d1ce03df915e694dcfa3f4e8714369cce2346686dbe0bc8e3f135e7"}, + {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9571de0c53db5cbc265945e08f093f093af2c5a11e14772c72d8e37fceeedd08"}, + {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7cba16b26efe774c096a5e822e4f27097b7c81ed6fb5264a2b3f5fd8784bab30"}, + {file = "ujson-5.8.0-cp312-cp312-win32.whl", hash = "sha256:48c7d373ff22366eecfa36a52b9b55b0ee5bd44c2b50e16084aa88b9de038916"}, + {file = "ujson-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ac97b1e182d81cf395ded620528c59f4177eee024b4b39a50cdd7b720fdeec6"}, + {file = "ujson-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2a64cc32bb4a436e5813b83f5aab0889927e5ea1788bf99b930fad853c5625cb"}, + {file = "ujson-5.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e54578fa8838ddc722539a752adfce9372474114f8c127bb316db5392d942f8b"}, + {file = "ujson-5.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9721cd112b5e4687cb4ade12a7b8af8b048d4991227ae8066d9c4b3a6642a582"}, + {file = "ujson-5.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d9707e5aacf63fb919f6237d6490c4e0244c7f8d3dc2a0f84d7dec5db7cb54c"}, + {file = "ujson-5.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0be81bae295f65a6896b0c9030b55a106fb2dec69ef877253a87bc7c9c5308f7"}, + {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae7f4725c344bf437e9b881019c558416fe84ad9c6b67426416c131ad577df67"}, + {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9ab282d67ef3097105552bf151438b551cc4bedb3f24d80fada830f2e132aeb9"}, + {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:94c7bd9880fa33fcf7f6d7f4cc032e2371adee3c5dba2922b918987141d1bf07"}, + {file = "ujson-5.8.0-cp38-cp38-win32.whl", hash = "sha256:bf5737dbcfe0fa0ac8fa599eceafae86b376492c8f1e4b84e3adf765f03fb564"}, + {file = "ujson-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:11da6bed916f9bfacf13f4fc6a9594abd62b2bb115acfb17a77b0f03bee4cfd5"}, + {file = "ujson-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:69b3104a2603bab510497ceabc186ba40fef38ec731c0ccaa662e01ff94a985c"}, + {file = "ujson-5.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9249fdefeb021e00b46025e77feed89cd91ffe9b3a49415239103fc1d5d9c29a"}, + {file = "ujson-5.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2873d196725a8193f56dde527b322c4bc79ed97cd60f1d087826ac3290cf9207"}, + {file = "ujson-5.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4dafa9010c366589f55afb0fd67084acd8added1a51251008f9ff2c3e44042"}, + {file = "ujson-5.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a42baa647a50fa8bed53d4e242be61023bd37b93577f27f90ffe521ac9dc7a3"}, + {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f3554eaadffe416c6f543af442066afa6549edbc34fe6a7719818c3e72ebfe95"}, + {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fb87decf38cc82bcdea1d7511e73629e651bdec3a43ab40985167ab8449b769c"}, + {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:407d60eb942c318482bbfb1e66be093308bb11617d41c613e33b4ce5be789adc"}, + {file = "ujson-5.8.0-cp39-cp39-win32.whl", hash = "sha256:0fe1b7edaf560ca6ab023f81cbeaf9946a240876a993b8c5a21a1c539171d903"}, + {file = "ujson-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f9b63530a5392eb687baff3989d0fb5f45194ae5b1ca8276282fb647f8dcdb3"}, + {file = "ujson-5.8.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:efeddf950fb15a832376c0c01d8d7713479fbeceaed1eaecb2665aa62c305aec"}, + {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d8283ac5d03e65f488530c43d6610134309085b71db4f675e9cf5dff96a8282"}, + {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb0142f6f10f57598655340a3b2c70ed4646cbe674191da195eb0985a9813b83"}, + {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d459aca895eb17eb463b00441986b021b9312c6c8cc1d06880925c7f51009c"}, + {file = "ujson-5.8.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d524a8c15cfc863705991d70bbec998456a42c405c291d0f84a74ad7f35c5109"}, + {file = "ujson-5.8.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d6f84a7a175c75beecde53a624881ff618e9433045a69fcfb5e154b73cdaa377"}, + {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b748797131ac7b29826d1524db1cc366d2722ab7afacc2ce1287cdafccddbf1f"}, + {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e72ba76313d48a1a3a42e7dc9d1db32ea93fac782ad8dde6f8b13e35c229130"}, + {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f504117a39cb98abba4153bf0b46b4954cc5d62f6351a14660201500ba31fe7f"}, + {file = "ujson-5.8.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8c91b6f4bf23f274af9002b128d133b735141e867109487d17e344d38b87d94"}, + {file = "ujson-5.8.0.tar.gz", hash = "sha256:78e318def4ade898a461b3d92a79f9441e7e0e4d2ad5419abed4336d702c7425"}, +] + +[[package]] +name = "urllib3" +version = "2.1.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "c327cbca046462e1faaf6f73f73cfc9d73b877b2547077deceeb262939248a96" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5041470 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[tool.poetry] +name = "ccm" +version = "0.1.0" +description = "Dependencies for helper scripts" +authors = ["Denis Krienbühl "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +typer = "^0.9.0" +pygithub = "^2.1.1" +requests = "^2.31.0" +semver = "^3.0.2" +pydantic = "^2.5.2" +click = "^8.1.7" + +[tool.poetry.group.dev.dependencies] +mypy = "^1.7.1" +python-lsp-server = {extras = ["flake8"], version = "^1.9.0"} +pylsp-mypy = "^0.6.8" +types-requests = "^2.31.0.10" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.mypy] +allow_redefinition = true + +[tool.pylsp-mypy] +enabled = true +live_mode = true +strict = true From 182a945ff36a755c6be57d9e09a42b439002a24b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Wed, 20 Dec 2023 14:45:32 +0100 Subject: [PATCH 07/15] Decrease integration test flakiness --- pkg/internal/integration/service_test.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/internal/integration/service_test.go b/pkg/internal/integration/service_test.go index 9b0f971..0c624cf 100644 --- a/pkg/internal/integration/service_test.go +++ b/pkg/internal/integration/service_test.go @@ -194,9 +194,13 @@ func (s *IntegrationTestSuite) TestServiceEndToEnd() { // Ensure that we get responses from two different pods (round-robin) s.T().Log("Verifying hostname service responses") responses := make(map[string]int) + errors := 0 for i := 0; i < 100; i++ { response, err := testkit.HelloNginx(addr, 80) - s.Assert().NoError(err) + if err != nil { + s.T().Logf("Request %d failed: %s", i, err) + errors++ + } if response != nil { s.Assert().NotEmpty(response.ServerName) @@ -206,6 +210,10 @@ func (s *IntegrationTestSuite) TestServiceEndToEnd() { time.Sleep(5 * time.Millisecond) } + // Allow for one error, which occurs maybe once in the first 100 requests + // to a service, and which does not occur anymore later (even when + // running for a long time). + s.Assert().LessOrEqual(errors, 1) s.Assert().Len(responses, 2) // In this simple case we expect no errors nor warnings From d17e1d1921bda121a37b8c04afdffe071f94dc07 Mon Sep 17 00:00:00 2001 From: Alain Kaeslin Date: Thu, 21 Dec 2023 14:03:06 +0100 Subject: [PATCH 08/15] Fix some typos and similar. --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4558572..6e935a3 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ You can access the created cluster as follows: ```bash # Via kubectl -export KUBECONFIG = k8test/cluster/admin.conf +export KUBECONFIG=k8test/cluster/admin.conf kubectl get nodes # Via ssh @@ -51,7 +51,7 @@ To cleanup: helpers/cleanup ``` -> :warning: This may incur costs on your side. Clusters that are removed may also leave behind loadbalancers, if associated services are not removed first. Please look at https://control.cloudscale.ch after cleanup to ensure that everything was removed. +> :warning: This may incur costs on your side. Clusters that are removed may also leave behind load balancers, if associated services are not removed first. Please look at https://control.cloudscale.ch after cleanup to ensure that everything was removed. # Operator Manual @@ -67,13 +67,13 @@ kubelet --cloud-provider=external This should be persisted indefinitely, depending on your distribution. Feel free to open an issue if you have trouble locating the right place to do this in your setup. -A cluster careted this way **will start all nodes tainted** as follows: +A cluster created this way **will start all nodes tainted** as follows: ```yaml node.cloudprovider.kubernetes.io/uninitialized: true ``` -This taint will be removed, once the CCM has initalized the nodes. +This taint will be removed, once the CCM has initialized the nodes. ### Node IPs @@ -119,7 +119,7 @@ To install the latest version: kubectl apply -f https://github.com/cloudscale-ch/cloudscale-cloud-controller-manager/releases/latest/download/config.yml ``` -To install a specific version, or to upgrade to a new version, have a look at the [list of releases](https://github.com/cloudscale-ch/cloudscale-cloud-controller-manager/releases]. +To install a specific version, or to upgrade to a new version, have a look at the [list of releases](https://github.com/cloudscale-ch/cloudscale-cloud-controller-manager/releases). Each release has a version-specific `kubectl apply` command in its release description. @@ -151,8 +151,8 @@ You should also find a `ProviderID` spec on each node. ## Releases -Relases are not integrated into the CI process. This remains a manual step, as releases via tagging via GitHub tend to be finicky and hard to control precisely. -Instead there is a release CLI, which ensures that a release is tested, before uploading the tested container image to Quay.io and publishing a new release. +Releases are not integrated into the CI process. This remains a manual step, as releases via tagging via GitHub tend to be finicky and hard to control precisely. +Instead, there is a release CLI, which ensures that a release is tested, before uploading the tested container image to Quay.io and publishing a new release. There are two ways to create a release: @@ -195,5 +195,5 @@ export QUAY_BOT_PASS="..." helpers/release create minor # Create a new minor pre-release for a test branch -helpers/relese create minor --pre --ref test/branch +helpers/release create minor --pre --ref test/branch ``` From 0baf0341c8e63fe703b3d68b8553464bb4feb7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Fri, 22 Dec 2023 13:31:43 +0100 Subject: [PATCH 09/15] Improve secret key documentation paragraphs --- README.md | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6e935a3..d1212ca 100644 --- a/README.md +++ b/README.md @@ -91,20 +91,10 @@ See https://github.com/kubernetes/kubernetes/pull/121028 To configure the CCM, the following secret needs to be configured: -```yaml -apiVersion: v1 -kind: Secret -metadata: - name: cloudscale - namespace: kube-system -stringData: - access-token: "..." -``` - -Create a file like this named `cloudscale-api-token.yml`, with your token filled in, and run the following: - ```bash -kubectl apply -f cloudscale-api-token.yml +kubectl create secret generic cloudscale \ + --from-literal=access-token='...' \ + --namespace kube-system ``` You can get a token on https://control.cloudscale.ch. Be aware that you need a read/write token. The token should not be deleted while it is in use, so we recommend naming the token accordingly. From 660ac45dc6e16c2c262fb6c1703d09c53a99647d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Fri, 22 Dec 2023 13:31:56 +0100 Subject: [PATCH 10/15] Fix missing tempdir --- helpers/release | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helpers/release b/helpers/release index 8aa1fb8..c388a27 100755 --- a/helpers/release +++ b/helpers/release @@ -335,7 +335,8 @@ class ReleaseAPI(BaseModel): ('shasum', '--check', 'image.tar.sha256'), cwd=tempdir) output = subprocess.check_output( - ('docker', 'load', '-i', 'image.tar')).decode('utf-8') + ('docker', 'load', '-i', 'image.tar'), cwd=tempdir)\ + .decode('utf-8') for line in output.splitlines(): if line.startswith('Loaded image: '): From 574f1d07d1df8df76398aebd58a2402463e19aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Thu, 4 Jan 2024 14:38:07 +0100 Subject: [PATCH 11/15] Show a better error message when no node is available This can happen if a single node cluster is used in its default configuration. In this case the following label causes Kubernetes to ask for a loadbalancer with no nodes: ```yaml node.kubernetes.io/exclude-from-external-load-balancers: "" ``` --- pkg/cloudscale_ccm/loadbalancer.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/cloudscale_ccm/loadbalancer.go b/pkg/cloudscale_ccm/loadbalancer.go index a26232a..30a93ff 100644 --- a/pkg/cloudscale_ccm/loadbalancer.go +++ b/pkg/cloudscale_ccm/loadbalancer.go @@ -248,6 +248,14 @@ func (l *loadbalancer) EnsureLoadBalancer( return nil, err } + // Refuse to do anything if there are no nodes + if len(nodes) == 0 { + return nil, fmt.Errorf( + "no valid nodes for service found, please verify there is " + + "at least one that allows load balancers", + ) + } + // Reconcile err := reconcileLbState(ctx, l.lbs.client, func() (*lbState, error) { // Get the desired state from Kubernetes From 253641c007080c973fb85a7eb56ede3f49254dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Fri, 5 Jan 2024 10:12:09 +0100 Subject: [PATCH 12/15] Do not declare exist/shutdown without ProviderID This change ensures that CCM does not declar nodes as running/shutdown or existing/missing, if there is no ProviderID found on the node. Queried with such nodes, the CCM will now return an error. This ensures that the node lifecycle controller, that calls these functions, does not act unless a ProviderID is set. This prevents a badly named server/node, that does not have a ProviderID, from falsly being declared as shutdown or missing. In practice, nodes get a ProviderID from the CCM as soon as they join, so this should have no real effect, but may act as a failsafe when other things go wrong. --- pkg/cloudscale_ccm/instances.go | 15 ++++++++++ pkg/cloudscale_ccm/instances_test.go | 42 ++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/pkg/cloudscale_ccm/instances.go b/pkg/cloudscale_ccm/instances.go index b42626f..190fd99 100644 --- a/pkg/cloudscale_ccm/instances.go +++ b/pkg/cloudscale_ccm/instances.go @@ -21,6 +21,17 @@ type instances struct { func (i *instances) InstanceExists(ctx context.Context, node *v1.Node) ( bool, error) { + // When a node does not have a ProviderID, return an err. `InstanceExists` + // is used by the node lifecycle controller to decide if a `NotReady` + // node can be removed and names are too weak of an association to say for + // sure. + // + // This should not really happen anyway, since the node will be enriched + // with a ProviderID as soon as it joins, but better safe than sorry. + if node.Spec.ProviderID == "" { + return false, fmt.Errorf("node %s has no ProviderID", node.Name) + } + server, err := i.srv.findByNode(ctx, node).AtMostOne() if err != nil { @@ -50,6 +61,10 @@ func (i *instances) InstanceExists(ctx context.Context, node *v1.Node) ( func (i *instances) InstanceShutdown(ctx context.Context, node *v1.Node) ( bool, error) { + if node.Spec.ProviderID == "" { + return false, fmt.Errorf("node %s has no ProviderID", node.Name) + } + server, err := i.srv.findByNode(ctx, node).One() if err != nil { diff --git a/pkg/cloudscale_ccm/instances_test.go b/pkg/cloudscale_ccm/instances_test.go index 59851cc..880e02c 100644 --- a/pkg/cloudscale_ccm/instances_test.go +++ b/pkg/cloudscale_ccm/instances_test.go @@ -27,12 +27,16 @@ func TestInstanceExists(t *testing.T) { assert.Equal(t, exists, actual) } - // By name - assertExists(true, testkit.NewNode("foo").V1()) - assertExists(true, testkit.NewNode("bar").V1()) - assertExists(false, testkit.NewNode("baz").V1()) + assertError := func(node *v1.Node) { + _, err := i.InstanceExists(context.Background(), node) + assert.Error(t, err) + } + + // Only decide if instances exist if they have a provider id + assertError(testkit.NewNode("foo").V1()) + assertError(testkit.NewNode("bar").V1()) + assertError(testkit.NewNode("baz").V1()) - // Provider id has precedence assertExists(true, testkit.NewNode("baz").WithProviderID( "cloudscale://5ac4afba-57b3-40d7-b34a-9da7056176fd").V1()) assertExists(false, testkit.NewNode("foo").WithProviderID( @@ -42,9 +46,12 @@ func TestInstanceExists(t *testing.T) { func TestInstanceShutdown(t *testing.T) { server := testkit.NewMockAPIServer() server.WithServers([]cloudscale.Server{ - {Name: "foo", Status: "stopped"}, - {Name: "bar", Status: "started"}, - {Name: "baz", Status: "changing"}, + {UUID: "c2e4aabd-8c91-46da-b069-000000000001", + Name: "foo", Status: "stopped"}, + {UUID: "c2e4aabd-8c91-46da-b069-000000000002", + Name: "bar", Status: "started"}, + {UUID: "c2e4aabd-8c91-46da-b069-000000000003", + Name: "baz", Status: "changing"}, }) server.Start() defer server.Close() @@ -57,9 +64,22 @@ func TestInstanceShutdown(t *testing.T) { assert.Equal(t, shutdown, actual) } - assertShutdown(true, testkit.NewNode("foo").V1()) - assertShutdown(false, testkit.NewNode("bar").V1()) - assertShutdown(false, testkit.NewNode("baz").V1()) + assertError := func(node *v1.Node) { + _, err := i.InstanceExists(context.Background(), node) + assert.Error(t, err) + } + + // Only decide if instances are shut down if they have a provider id + assertError(testkit.NewNode("foo").V1()) + assertError(testkit.NewNode("bar").V1()) + assertError(testkit.NewNode("baz").V1()) + + assertShutdown(true, testkit.NewNode("foo").WithProviderID( + "cloudscale://c2e4aabd-8c91-46da-b069-000000000001").V1()) + assertShutdown(false, testkit.NewNode("bar").WithProviderID( + "cloudscale://c2e4aabd-8c91-46da-b069-000000000002").V1()) + assertShutdown(false, testkit.NewNode("baz").WithProviderID( + "cloudscale://c2e4aabd-8c91-46da-b069-000000000003").V1()) // If the node cannot be found, we rather err, than make any statement // about it being shutdown or not (that's the job of InstanceExists) From 6bf7e03158e26f22c628db78b2ea149f6e06091b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Fri, 5 Jan 2024 13:04:35 +0100 Subject: [PATCH 13/15] Add example and settings docs to README [skip ci] --- README.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/README.md b/README.md index d1212ca..4fe4a85 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,74 @@ helpers/cleanup > :warning: This may incur costs on your side. Clusters that are removed may also leave behind load balancers, if associated services are not removed first. Please look at https://control.cloudscale.ch after cleanup to ensure that everything was removed. +### Node Metadata Example + +Once installed, the CCM will enrich nodes with metadata like the following: + +```yaml +metadata: + labels: + node.kubernetes.io/instance-type: plus-32-16 + topology.kubernetes.io/region: lpg + topology.kubernetes.io/zone: lpg1 +spec: + providerID: cloudscale:// +status: + addresses: + - address: k8test-worker-1 + type: Hostname + - address: 5.102.148.123 + type: ExternalIP + - address: 2a06:c01:1000:1165::123 + type: ExternalIP + - address: 10.1.1.123 + type: InternalIP +``` + +### LoadBalancer Example + +To run a simple loadbalanced service, you can use the following example: + +```bash +kubectl create deployment hello \ + --image=nginxdemos/hello:plain-text \ + --replicas=2 +kubectl expose deployment hello \ + --name=hello \ + --type=LoadBalancer \ + --port=80 \ + --target-port=80 \ +``` + +Afterward, wait for the external IP to become available: + +```bash +kubectl get service hello --watch +``` + +Details and some progress messages are visible here: + +```bash +kubectl describe service hello +``` + +To check the CCM log, run the following: + +```bash +kubectl logs -l k8s-app=cloudscale-cloud-controller-manager -n kube-system +``` + +Once the external IP is available, you can use it to check the result: + +```bash +$ curl 5.102.148.123 +Server address: 5.102.148.123:80 +Server name: hello-7766f96cd-m7pvk +Date: 05/Jan/2024:10:20:18 +0000 +URI: / +Request ID: dbe6be294e3280b6ff3b919abf20e9f9 +``` + # Operator Manual ## Installation @@ -137,6 +205,32 @@ You should also find a `ProviderID` spec on each node. > :warning: These instructions may not be right for your cluster, so be sure to test this in a staging environment. +### LoadBalancer Service Configuration + +You can influence the way services of type `LoadBalancer` are created by the CCM. To do so, set annotations on the service resource: + +```yaml +apiversion: v1 +kind: Service +metadata: + annotations: + k8s.cloudscale.ch/loadbalancer-listener-allowed-cidrs: '["1.2.3.0/24"]' +``` + +The full set of configuration toggles can be found in the [`pkg/cloudscale_ccm/loadbalancer.go`](pkg/cloudscale/ccm/loadbalancer.go) file. + +These annotations are all optional as they come with reasonable defaults. + +### Preserve Client Source IP + +By default, the source IP seen in the target container is not the original source IP of the client. + +To change this, see the official Kubernetes documentation: + +https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip + +The mentioned `externalTrafficPolicy: Local` setting on the service spec is fully supported. + # Developer Manual ## Releases From 4b0b95ef37a4e980fd19a050f77f4ea74630e4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Fri, 5 Jan 2024 13:19:09 +0100 Subject: [PATCH 14/15] Drop the "bot" in the quay username/password Whether it is a bot or not is irrelevant for the release process. [skip ci] --- README.md | 8 ++++---- helpers/release | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4fe4a85..6dd5ae6 100644 --- a/README.md +++ b/README.md @@ -264,16 +264,16 @@ A fine-grained GitHub access token with the following properties: - Commit statuses: Read-Only. - Contents: Read and Write. -**`QUAY_BOT_USER` / `QUAY_BOT_PASS`** +**`QUAY_USER` / `QUAY_PASS`** -Quay bot user with permission to write to the `cloudscalech/cloudscale-cloud-controller-manager` repository on quay.io. +Quay user with permission to write to the `cloudscalech/cloudscale-cloud-controller-manager` repository on quay.io. You can then use `helpers/release create` to create a new release: ```bash export GITHUB_TOKEN="github_pat_..." -export QUAY_BOT_USER="..." -export QUAY_BOT_PASS="..." +export QUAY_USER="..." +export QUAY_PASS="..." # Create a new minor release of the main branch helpers/release create minor diff --git a/helpers/release b/helpers/release index c388a27..8e46df5 100755 --- a/helpers/release +++ b/helpers/release @@ -74,7 +74,7 @@ class ReleaseAPI(BaseModel): # - Contents: Read and Write. github_token: str - # A quay bot username/token with write access to IMAGE_REPO. + # A quay username/password tuple with write access to IMAGE_REPO. quay_auth: Tuple[str, str] @cached_property @@ -416,8 +416,8 @@ class ReleaseAPI(BaseModel): @cli.command(name='create') def create_command( increment: Annotated[Increment, Argument()], - quay_bot_user: Annotated[str, Option(envvar='QUAY_BOT_USER')], - quay_bot_pass: Annotated[str, Option(envvar='QUAY_BOT_PASS')], + quay_user: Annotated[str, Option(envvar='QUAY_USER')], + quay_pass: Annotated[str, Option(envvar='QUAY_PASS')], github_token: Annotated[str, Option(envvar='GITHUB_TOKEN')], pre: Annotated[bool, Option()] = False, ref: Annotated[str, Option()] = 'main', @@ -428,7 +428,7 @@ def create_command( api = ReleaseAPI( github_token=github_token, - quay_auth=(quay_bot_user, quay_bot_pass), + quay_auth=(quay_user, quay_pass), ) try: From b06bb0f8845fa3d7b038b3eb93601b27f67bb6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Krienb=C3=BChl?= Date: Fri, 5 Jan 2024 13:45:42 +0100 Subject: [PATCH 15/15] Switch to 'preview' state --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6dd5ae6..8b91680 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kubernetes Cloud Controller Manager for cloudscale.ch -> :warning: This is currently a work in-progress and not yet ready to be used. +> :warning: This is currently a preview and not yet read for production Integrate your Kubernetes cluster with cloudscale.ch infrastructure, with our cloud controller manager (CCM).