diff --git a/.github/workflows/additional_demo_notebook_tests.yaml b/.github/workflows/additional_demo_notebook_tests.yaml index 03201e10..096cb509 100644 --- a/.github/workflows/additional_demo_notebook_tests.yaml +++ b/.github/workflows/additional_demo_notebook_tests.yaml @@ -15,7 +15,7 @@ env: jobs: verify-local_interactive: if: ${{ github.event.label.name == 'test-additional-notebooks' }} - runs-on: ubuntu-20.04-4core + runs-on: ubuntu-latest-4core steps: - name: Checkout code @@ -50,7 +50,7 @@ jobs: - name: Set up specific Python version uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.11' cache: 'pip' # caching pip dependencies - name: Setup and start KinD cluster @@ -133,7 +133,7 @@ jobs: verify-ray_job_client: if: ${{ github.event.label.name == 'test-additional-notebooks' }} - runs-on: ubuntu-20.04-4core + runs-on: ubuntu-latest-4core steps: - name: Checkout code @@ -168,7 +168,7 @@ jobs: - name: Set up specific Python version uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.11' cache: 'pip' # caching pip dependencies - name: Setup and start KinD cluster diff --git a/.github/workflows/coverage-badge.yaml b/.github/workflows/coverage-badge.yaml index bae1212d..d793a699 100644 --- a/.github/workflows/coverage-badge.yaml +++ b/.github/workflows/coverage-badge.yaml @@ -4,29 +4,32 @@ name: Coverage Badge on: push: - branches: [ main ] + branches: [ main, ray-jobs-feature ] jobs: report: + permissions: + contents: write + pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python 3.9 + - name: Set up Python 3.11 uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.11 - name: Install dependencies run: | python -m pip install --upgrade pip pip install poetry poetry config virtualenvs.create false - poetry lock --no-update + poetry lock poetry install --with test - name: Generate coverage report run: | - coverage run -m pytest + coverage run --omit="src/**/test_*.py,src/codeflare_sdk/common/utils/unit_test_support.py,src/codeflare_sdk/vendored/**" -m pytest - name: Coverage Badge uses: tj-actions/coverage-badge-py@v2 @@ -47,7 +50,14 @@ jobs: - name: Create Pull Request if: steps.changed_files.outputs.files_changed == 'true' - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.GITHUB_TOKEN }} title: "[Automatic] Coverage Badge Update" + commit-message: "Updated coverage.svg" + branch: create-pull-request/coverage-badge-update + delete-branch: true + body: | + This is an automated pull request to update the coverage badge. + + - Updated coverage.svg based on latest test results diff --git a/.github/workflows/e2e_tests.yaml b/.github/workflows/e2e_tests.yaml index fea42ab6..44bf1214 100644 --- a/.github/workflows/e2e_tests.yaml +++ b/.github/workflows/e2e_tests.yaml @@ -5,12 +5,13 @@ on: pull_request: branches: - main - - 'release-*' + - "release-*" + - ray-jobs-feature paths-ignore: - - 'docs/**' - - '**.adoc' - - '**.md' - - 'LICENSE' + - "docs/**" + - "**.adoc" + - "**.md" + - "LICENSE" concurrency: group: ${{ github.head_ref }}-${{ github.workflow }} @@ -21,7 +22,7 @@ env: jobs: kubernetes: - runs-on: ubuntu-20.04-4core-gpu + runs-on: gpu-t4-4-core steps: - name: Checkout code @@ -32,9 +33,9 @@ jobs: - name: Checkout common repo code uses: actions/checkout@v4 with: - repository: 'project-codeflare/codeflare-common' - ref: 'main' - path: 'common' + repository: "project-codeflare/codeflare-common" + ref: "main" + path: "common" - name: Checkout CodeFlare operator repository uses: actions/checkout@v4 @@ -45,7 +46,7 @@ jobs: - name: Set Go uses: actions/setup-go@v5 with: - go-version-file: './codeflare-operator/go.mod' + go-version-file: "./codeflare-operator/go.mod" cache-dependency-path: "./codeflare-operator/go.sum" - name: Set up gotestfmt @@ -56,7 +57,7 @@ jobs: - name: Set up specific Python version uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.12' cache: 'pip' # caching pip dependencies - name: Setup NVidia GPU environment for KinD @@ -75,7 +76,7 @@ jobs: run: | cd codeflare-operator echo Setting up CodeFlare stack - make setup-e2e + make setup-e2e KUEUE_VERSION=v0.13.4 KUBERAY_VERSION=v1.4.0 echo Deploying CodeFlare operator make deploy -e IMG="${CODEFLARE_OPERATOR_IMG}" -e ENV="e2e" kubectl wait --timeout=120s --for=condition=Available=true deployment -n openshift-operators codeflare-operator-manager @@ -94,6 +95,10 @@ jobs: kubectl create clusterrolebinding sdk-user-namespace-creator --clusterrole=namespace-creator --user=sdk-user kubectl create clusterrole raycluster-creator --verb=get,list,create,delete,patch --resource=rayclusters kubectl create clusterrolebinding sdk-user-raycluster-creator --clusterrole=raycluster-creator --user=sdk-user + kubectl create clusterrole rayjob-creator --verb=get,list,create,delete,patch --resource=rayjobs + kubectl create clusterrolebinding sdk-user-rayjob-creator --clusterrole=rayjob-creator --user=sdk-user + kubectl create clusterrole rayjob-status-reader --verb=get,list,patch,update --resource=rayjobs/status + kubectl create clusterrolebinding sdk-user-rayjob-status-reader --clusterrole=rayjob-status-reader --user=sdk-user kubectl create clusterrole appwrapper-creator --verb=get,list,create,delete,patch --resource=appwrappers kubectl create clusterrolebinding sdk-user-appwrapper-creator --clusterrole=appwrapper-creator --user=sdk-user kubectl create clusterrole resourceflavor-creator --verb=get,list,create,delete --resource=resourceflavors @@ -104,8 +109,12 @@ jobs: kubectl create clusterrolebinding sdk-user-localqueue-creator --clusterrole=localqueue-creator --user=sdk-user kubectl create clusterrole list-secrets --verb=get,list --resource=secrets kubectl create clusterrolebinding sdk-user-list-secrets --clusterrole=list-secrets --user=sdk-user - kubectl create clusterrole pod-creator --verb=get,list --resource=pods + kubectl create clusterrole pod-creator --verb=get,list,watch --resource=pods kubectl create clusterrolebinding sdk-user-pod-creator --clusterrole=pod-creator --user=sdk-user + kubectl create clusterrole service-reader --verb=get,list,watch --resource=services + kubectl create clusterrolebinding sdk-user-service-reader --clusterrole=service-reader --user=sdk-user + kubectl create clusterrole port-forward-pods --verb=create --resource=pods/portforward + kubectl create clusterrolebinding sdk-user-port-forward-pods-binding --clusterrole=port-forward-pods --user=sdk-user kubectl config use-context sdk-user - name: Run e2e tests @@ -117,7 +126,7 @@ jobs: pip install poetry poetry install --with test,docs echo "Running e2e tests..." - poetry run pytest -v -s ./tests/e2e -m 'kind and nvidia_gpu' > ${CODEFLARE_TEST_OUTPUT_DIR}/pytest_output.log 2>&1 + poetry run pytest -v -s ./tests/e2e/ -m 'kind and nvidia_gpu' > ${CODEFLARE_TEST_OUTPUT_DIR}/pytest_output.log 2>&1 env: GRPC_DNS_RESOLVER: "native" @@ -141,7 +150,13 @@ jobs: if: always() && steps.deploy.outcome == 'success' run: | echo "Printing KubeRay operator logs" - kubectl logs -n ray-system --tail -1 -l app.kubernetes.io/name=kuberay | tee ${CODEFLARE_TEST_OUTPUT_DIR}/kuberay.log + kubectl logs -n default --tail -1 -l app.kubernetes.io/name=kuberay | tee ${CODEFLARE_TEST_OUTPUT_DIR}/kuberay.log + + - name: Print Kueue controller logs + if: always() && steps.deploy.outcome == 'success' + run: | + echo "Printing Kueue controller logs" + kubectl logs -n kueue-system --tail -1 -l control-plane=controller-manager | tee ${CODEFLARE_TEST_OUTPUT_DIR}/kueue.log - name: Export all KinD pod logs uses: ./common/github-actions/kind-export-logs diff --git a/.github/workflows/guided_notebook_tests.yaml b/.github/workflows/guided_notebook_tests.yaml index 7a77d5a3..3309c6a1 100644 --- a/.github/workflows/guided_notebook_tests.yaml +++ b/.github/workflows/guided_notebook_tests.yaml @@ -3,6 +3,7 @@ name: Guided notebooks tests on: pull_request: branches: [ main ] + types: [ labeled ] concurrency: group: ${{ github.head_ref }}-${{ github.workflow }} @@ -14,7 +15,7 @@ env: jobs: verify-0_basic_ray: if: ${{ contains(github.event.pull_request.labels.*.name, 'test-guided-notebooks') }} - runs-on: ubuntu-20.04-4core + runs-on: ubuntu-latest-4core steps: - name: Checkout code @@ -49,7 +50,7 @@ jobs: - name: Set up specific Python version uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.11' cache: 'pip' # caching pip dependencies - name: Setup and start KinD cluster @@ -125,7 +126,7 @@ jobs: verify-1_cluster_job_client: if: ${{ contains(github.event.pull_request.labels.*.name, 'test-guided-notebooks') }} - runs-on: ubuntu-20.04-4core-gpu + runs-on: gpu-t4-4-core steps: - name: Checkout code @@ -160,7 +161,7 @@ jobs: - name: Set up specific Python version uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.11' cache: 'pip' # caching pip dependencies - name: Setup NVidia GPU environment for KinD @@ -247,7 +248,7 @@ jobs: verify-2_basic_interactive: if: ${{ contains(github.event.pull_request.labels.*.name, 'test-guided-notebooks') }} - runs-on: ubuntu-20.04-4core-gpu + runs-on: gpu-t4-4-core steps: - name: Checkout code @@ -282,7 +283,7 @@ jobs: - name: Set up specific Python version uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.11' cache: 'pip' # caching pip dependencies - name: Setup NVidia GPU environment for KinD diff --git a/.github/workflows/odh-notebooks-sync.yml b/.github/workflows/odh-notebooks-sync.yml index f0853bff..91f5aecb 100644 --- a/.github/workflows/odh-notebooks-sync.yml +++ b/.github/workflows/odh-notebooks-sync.yml @@ -33,11 +33,11 @@ env: REPO_OWNER: ${{ github.event.inputs.codeflare-repository-organization }} REPO_NAME: notebooks GITHUB_TOKEN: ${{ secrets.CODEFLARE_MACHINE_ACCOUNT_TOKEN }} - MINIMUM_SUPPORTED_PYTHON_VERSION: 3.9 + MINIMUM_SUPPORTED_PYTHON_VERSION: 3.11 jobs: build: - runs-on: ubuntu-22.04-8core + runs-on: ubuntu-latest-8core steps: - name: Clone repository and Sync run: | @@ -56,10 +56,8 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} cache: 'pipenv' - # Sync fails with pipenv 2024.1.0 (current latest version) - # TODO: We should retry with later versions of pipenv once they are available. - name: Install pipenv and pip-versions - run: pip install pipenv==2024.0.3 pip-versions + run: pip install pipenv==2024.4.0 pip-versions - name: Update Pipfiles in accordance with Codeflare-SDK latest release run: | @@ -74,7 +72,7 @@ jobs: # replace existing version of cf-sdk with new version in Pipfile sed -i "s/codeflare-sdk = .*$/codeflare-sdk = \"~=$CODEFLARE_RELEASE_VERSION\"/g" Pipfile # Lock dependencies, ensuring pre-release are included and clear previous state - if ! pipenv lock --pre --clear ; then + if ! pipenv lock --verbose --pre --clear ; then echo "Failed to lock dependencies" exit 1 fi @@ -98,7 +96,9 @@ jobs: echo "Version ${CODEFLARE_RELEASE_VERSION} is available for $package_name" # list all Pipfile paths having Codeflare-SDK listed # Extracting only directories from file paths, excluding a `.gitworkflow` and `.git` directory - directories+=($(grep --exclude-dir=.git --exclude-dir=.github --include="Pipfile*" -rl "${package_name} = \"~=.*\"" | xargs dirname | sort | uniq)) + # Extracting Intel directories as they are not supported in RHOAI + # Removing tensorflow image TEMPORARILY until solution has been made for the tf2onnx package dependency resolution + directories+=($(grep --exclude-dir=.git --exclude-dir=.github --exclude-dir=intel --exclude-dir=tensorflow --exclude-dir=rocm-tensorflow --include="Pipfile*" -rl "${package_name} = \"~=.*\"" | xargs dirname | sort | uniq)) counter=0 total=${#directories[@]} for dir in "${directories[@]}"; do diff --git a/.github/workflows/publish-documentation.yaml b/.github/workflows/publish-documentation.yaml index 80afe7d6..a96891c3 100644 --- a/.github/workflows/publish-documentation.yaml +++ b/.github/workflows/publish-documentation.yaml @@ -19,7 +19,7 @@ jobs: - name: Install Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.11 - name: Install Sphinx run: | sudo apt-get update diff --git a/.github/workflows/rayjob_e2e_tests.yaml b/.github/workflows/rayjob_e2e_tests.yaml new file mode 100644 index 00000000..ba0659c0 --- /dev/null +++ b/.github/workflows/rayjob_e2e_tests.yaml @@ -0,0 +1,176 @@ +# rayjob e2e tests workflow for CodeFlare-SDK +name: rayjob-e2e + +on: + pull_request: + branches: + - main + - "release-*" + - ray-jobs-feature + paths-ignore: + - "docs/**" + - "**.adoc" + - "**.md" + - "LICENSE" + +concurrency: + group: ${{ github.head_ref }}-${{ github.workflow }} + cancel-in-progress: true + +env: + CODEFLARE_OPERATOR_IMG: "quay.io/project-codeflare/codeflare-operator:dev" + +jobs: + kubernetes-rayjob: + runs-on: gpu-t4-4-core + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Checkout common repo code + uses: actions/checkout@v4 + with: + repository: "project-codeflare/codeflare-common" + ref: "main" + path: "common" + + - name: Checkout CodeFlare operator repository + uses: actions/checkout@v4 + with: + repository: project-codeflare/codeflare-operator + path: codeflare-operator + + - name: Set Go + uses: actions/setup-go@v5 + with: + go-version-file: "./codeflare-operator/go.mod" + cache-dependency-path: "./codeflare-operator/go.sum" + + - name: Set up gotestfmt + uses: gotesttools/gotestfmt-action@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up specific Python version + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" # caching pip dependencies + + - name: Setup NVidia GPU environment for KinD + uses: ./common/github-actions/nvidia-gpu-setup + + - name: Setup and start KinD cluster + uses: ./common/github-actions/kind + with: + worker-nodes: 1 + + - name: Install NVidia GPU operator for KinD + uses: ./common/github-actions/nvidia-gpu-operator + + - name: Deploy CodeFlare stack + id: deploy + run: | + cd codeflare-operator + echo Setting up CodeFlare stack + make setup-e2e KUEUE_VERSION=v0.13.4 KUBERAY_VERSION=v1.4.0 + echo Deploying CodeFlare operator + make deploy -e IMG="${CODEFLARE_OPERATOR_IMG}" -e ENV="e2e" + kubectl wait --timeout=120s --for=condition=Available=true deployment -n openshift-operators codeflare-operator-manager + cd .. + + - name: Add user to KinD + uses: ./common/github-actions/kind-add-user + with: + user-name: sdk-user + + - name: Configure RBAC for sdk user with limited permissions + run: | + kubectl create clusterrole list-ingresses --verb=get,list --resource=ingresses + kubectl create clusterrolebinding sdk-user-list-ingresses --clusterrole=list-ingresses --user=sdk-user + kubectl create clusterrole namespace-creator --verb=get,list,create,delete,patch --resource=namespaces + kubectl create clusterrolebinding sdk-user-namespace-creator --clusterrole=namespace-creator --user=sdk-user + kubectl create clusterrole raycluster-creator --verb=get,list,create,delete,patch --resource=rayclusters + kubectl create clusterrolebinding sdk-user-raycluster-creator --clusterrole=raycluster-creator --user=sdk-user + kubectl create clusterrole rayjob-creator --verb=get,list,create,delete,patch --resource=rayjobs + kubectl create clusterrolebinding sdk-user-rayjob-creator --clusterrole=rayjob-creator --user=sdk-user + kubectl create clusterrole rayjob-status-reader --verb=get,list,patch,update --resource=rayjobs/status + kubectl create clusterrolebinding sdk-user-rayjob-status-reader --clusterrole=rayjob-status-reader --user=sdk-user + kubectl create clusterrole appwrapper-creator --verb=get,list,create,delete,patch --resource=appwrappers + kubectl create clusterrolebinding sdk-user-appwrapper-creator --clusterrole=appwrapper-creator --user=sdk-user + kubectl create clusterrole resourceflavor-creator --verb=get,list,create,delete --resource=resourceflavors + kubectl create clusterrolebinding sdk-user-resourceflavor-creator --clusterrole=resourceflavor-creator --user=sdk-user + kubectl create clusterrole clusterqueue-creator --verb=get,list,create,delete,patch --resource=clusterqueues + kubectl create clusterrolebinding sdk-user-clusterqueue-creator --clusterrole=clusterqueue-creator --user=sdk-user + kubectl create clusterrole localqueue-creator --verb=get,list,create,delete,patch --resource=localqueues + kubectl create clusterrolebinding sdk-user-localqueue-creator --clusterrole=localqueue-creator --user=sdk-user + kubectl create clusterrole list-secrets --verb=get,list --resource=secrets + kubectl create clusterrolebinding sdk-user-list-secrets --clusterrole=list-secrets --user=sdk-user + kubectl create clusterrole pod-creator --verb=get,list,watch --resource=pods + kubectl create clusterrolebinding sdk-user-pod-creator --clusterrole=pod-creator --user=sdk-user + kubectl create clusterrole service-reader --verb=get,list,watch --resource=services + kubectl create clusterrolebinding sdk-user-service-reader --clusterrole=service-reader --user=sdk-user + kubectl create clusterrole port-forward-pods --verb=create --resource=pods/portforward + kubectl create clusterrolebinding sdk-user-port-forward-pods-binding --clusterrole=port-forward-pods --user=sdk-user + kubectl create clusterrole secret-manager --verb=get,list,create,delete,update,patch --resource=secrets + kubectl create clusterrolebinding sdk-user-secret-manager --clusterrole=secret-manager --user=sdk-user + kubectl create clusterrole workload-reader --verb=get,list,watch --resource=workloads + kubectl create clusterrolebinding sdk-user-workload-reader --clusterrole=workload-reader --user=sdk-user + kubectl config use-context sdk-user + + - name: Run RayJob E2E tests + run: | + export CODEFLARE_TEST_OUTPUT_DIR=${{ env.TEMP_DIR }} + echo "CODEFLARE_TEST_OUTPUT_DIR=${CODEFLARE_TEST_OUTPUT_DIR}" >> $GITHUB_ENV + + set -euo pipefail + pip install poetry + poetry install --with test,docs + echo "Running RayJob e2e tests..." + poetry run pytest -v -s ./tests/e2e/rayjob/ > ${CODEFLARE_TEST_OUTPUT_DIR}/pytest_output_rayjob.log 2>&1 + + - name: Switch to kind-cluster context to print logs + if: always() && steps.deploy.outcome == 'success' + run: kubectl config use-context kind-cluster + + - name: Print Pytest output log + if: always() && steps.deploy.outcome == 'success' + run: | + echo "Printing Pytest output logs" + cat ${CODEFLARE_TEST_OUTPUT_DIR}/pytest_output_rayjob.log + + - name: Print CodeFlare operator logs + if: always() && steps.deploy.outcome == 'success' + run: | + echo "Printing CodeFlare operator logs" + kubectl logs -n openshift-operators --tail -1 -l app.kubernetes.io/name=codeflare-operator | tee ${CODEFLARE_TEST_OUTPUT_DIR}/codeflare-operator.log + + - name: Print KubeRay operator logs + if: always() && steps.deploy.outcome == 'success' + run: | + echo "Printing KubeRay operator logs" + kubectl logs -n default --tail -1 -l app.kubernetes.io/name=kuberay | tee ${CODEFLARE_TEST_OUTPUT_DIR}/kuberay.log + + - name: Print Kueue controller logs + if: always() && steps.deploy.outcome == 'success' + run: | + echo "Printing Kueue controller logs" + kubectl logs -n kueue-system --tail -1 -l control-plane=controller-manager | tee ${CODEFLARE_TEST_OUTPUT_DIR}/kueue.log + + - name: Export all KinD pod logs + uses: ./common/github-actions/kind-export-logs + if: always() && steps.deploy.outcome == 'success' + with: + output-directory: ${CODEFLARE_TEST_OUTPUT_DIR} + + - name: Upload logs + uses: actions/upload-artifact@v4 + if: always() && steps.deploy.outcome == 'success' + with: + name: logs + retention-days: 10 + path: | + ${{ env.CODEFLARE_TEST_OUTPUT_DIR }}/**/*.log diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ddc23b5a..c3e47dab 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -17,7 +17,7 @@ on: default: 'project-codeflare' python_version: type: string - default: "3.8" + default: "3.11" required: true poetry_version: type: string @@ -37,6 +37,7 @@ jobs: contents: write id-token: write # This permission is required for trusted publishing pull-requests: write # This permission is required for creating PRs + actions: write # This permission is required for running actions steps: - name: Checkout the repository uses: actions/checkout@v4 diff --git a/.github/workflows/ui_notebooks_test.yaml b/.github/workflows/ui_notebooks_test.yaml index 28f7e06c..1b5ad524 100644 --- a/.github/workflows/ui_notebooks_test.yaml +++ b/.github/workflows/ui_notebooks_test.yaml @@ -3,6 +3,7 @@ name: UI notebooks tests on: pull_request: branches: [ main ] + types: [ labeled ] concurrency: group: ${{ github.head_ref }}-${{ github.workflow }} @@ -14,7 +15,7 @@ env: jobs: verify-3_widget_example: if: ${{ contains(github.event.pull_request.labels.*.name, 'test-guided-notebooks') || contains(github.event.pull_request.labels.*.name, 'test-ui-notebooks') }} - runs-on: ubuntu-20.04-4core + runs-on: ubuntu-latest-4core steps: - name: Checkout code @@ -49,7 +50,7 @@ jobs: - name: Set up specific Python version uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.11" cache: "pip" # caching pip dependencies - name: Setup and start KinD cluster diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 267e1a6c..e276ee3e 100755 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -2,31 +2,31 @@ name: Python Tests on: pull_request: - branches: [ main ] + branches: [ main, ray-jobs-feature ] push: - branches: [ main ] + branches: [ main, ray-jobs-feature ] jobs: unit-tests: runs-on: ubuntu-latest - container: - image: quay.io/project-codeflare/codeflare-sdk-precommit:v0.0.3 steps: - uses: actions/checkout@v4 + + - name: Set up python + uses: actions/setup-python@v5 + with: + python-version: '3.11' - name: Install poetry run: pip install poetry - - uses: actions/setup-python@v5 - with: - python-version: '3.8' - - name: Install dependencies + - name: Install dependencies with poetry run: | poetry config virtualenvs.create false - poetry lock --no-update + poetry lock poetry install --with test - name: Test with pytest and check coverage run: | - coverage run -m pytest + coverage run --omit="src/**/test_*.py,src/codeflare_sdk/common/utils/unit_test_support.py,src/codeflare_sdk/vendored/**" -m pytest coverage=$(coverage report -m | tail -1 | tail -c 4 | head -c 2) if (( $coverage < 90 )); then echo "Coverage failed at ${coverage}%"; exit 1; else echo "Coverage passed, ${coverage}%"; fi - name: Upload to Codecov diff --git a/.github/workflows/update-versions.yaml b/.github/workflows/update-versions.yaml new file mode 100644 index 00000000..1eb253fb --- /dev/null +++ b/.github/workflows/update-versions.yaml @@ -0,0 +1,334 @@ +name: Update SDK, Ray, and CUDA Versions & Runtime Image SHAs + +on: + workflow_dispatch: + inputs: + new_sdk_version: + description: 'New SDK version (e.g., 0.32.0)' + required: false + type: string + default: '' + new_ray_version: + description: 'New Ray version (e.g., 2.48.0)' + required: false + type: string + default: '' + new_cuda_py311_sha: + description: 'New CUDA Python 3.11 runtime image SHA (e.g., abc123...)' + required: false + type: string + default: '' + new_cuda_py312_sha: + description: 'New CUDA Python 3.12 runtime image SHA (e.g., def456...)' + required: false + type: string + default: '' + new_cuda_version_py311: + description: 'New CUDA version for Python 3.11 (e.g., cu121)' + required: false + type: string + default: '' + new_cuda_version_py312: + description: 'New CUDA version for Python 3.12 (e.g., cu128)' + required: false + type: string + default: '' + +env: + PR_BRANCH_NAME: ray-version-update-${{ github.run_id }} + +jobs: + update-versions: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + + - name: Configure git and create branch + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git checkout main + git pull origin main + git checkout -b ${{ env.PR_BRANCH_NAME }} + + - name: Update constants.py + run: | + # Update Ray version if provided + if [ -n "${{ github.event.inputs.new_ray_version }}" ]; then + sed -i "s/RAY_VERSION = \"[^\"]*\"/RAY_VERSION = \"${{ github.event.inputs.new_ray_version }}\"/" src/codeflare_sdk/common/utils/constants.py + + # Update comments with new Ray version and CUDA versions if both are provided + if [ -n "${{ github.event.inputs.new_cuda_version_py311 }}" ]; then + sed -i "s/\* For python 3.11:ray:[^\"]*/\* For python 3.11:ray:${{ github.event.inputs.new_ray_version }}-py311-${{ github.event.inputs.new_cuda_version_py311 }}/" src/codeflare_sdk/common/utils/constants.py + fi + if [ -n "${{ github.event.inputs.new_cuda_version_py312 }}" ]; then + sed -i "s/\* For python 3.12:ray:[^\"]*/\* For python 3.12:ray:${{ github.event.inputs.new_ray_version }}-py312-${{ github.event.inputs.new_cuda_version_py312 }}/" src/codeflare_sdk/common/utils/constants.py + fi + fi + + # Update runtime image SHAs if provided + if [ -n "${{ github.event.inputs.new_cuda_py311_sha }}" ]; then + sed -i "s/CUDA_PY311_RUNTIME_IMAGE = \"quay\.io\/modh\/ray@sha256:[^\"]*\"/CUDA_PY311_RUNTIME_IMAGE = \"quay.io\/modh\/ray@sha256:${{ github.event.inputs.new_cuda_py311_sha }}\"/" src/codeflare_sdk/common/utils/constants.py + fi + + if [ -n "${{ github.event.inputs.new_cuda_py312_sha }}" ]; then + sed -i "s/CUDA_PY312_RUNTIME_IMAGE = \"quay\.io\/modh\/ray@sha256:[^\"]*\"/CUDA_PY312_RUNTIME_IMAGE = \"quay.io\/modh\/ray@sha256:${{ github.event.inputs.new_cuda_py312_sha }}\"/" src/codeflare_sdk/common/utils/constants.py + fi + + - name: Update pyproject.toml + run: | + # Update Ray dependency version in pyproject.toml if Ray version is provided + if [ -n "${{ github.event.inputs.new_ray_version }}" ]; then + sed -i "s/ray = {version = \"[^\"]*\", extras = \[\"data\", \"default\"\]}/ray = {version = \"${{ github.event.inputs.new_ray_version }}\", extras = [\"data\", \"default\"]}/" pyproject.toml + fi + + # Update SDK version in pyproject.toml if SDK version is provided + if [ -n "${{ github.event.inputs.new_sdk_version }}" ]; then + # Update both [project] and [tool.poetry] version fields + sed -i "s/^version = \"[^\"]*\"/version = \"${{ github.event.inputs.new_sdk_version }}\"/" pyproject.toml + fi + + - name: Update documentation files + run: | + # Update documentation files with new Ray version and image tags if provided + if [ -n "${{ github.event.inputs.new_ray_version }}" ] && [ -n "${{ github.event.inputs.new_cuda_version_py311 }}" ]; then + find docs/ -name "*.rst" -exec sed -i "s/quay\.io\/modh\/ray:[0-9]\+\.[0-9]\+\.[0-9]\+-py311-cu[0-9]\+/quay.io\/modh\/ray:${{ github.event.inputs.new_ray_version }}-py311-${{ github.event.inputs.new_cuda_version_py311 }}/g" {} \; + find docs/ -name "*.rst" -exec sed -i "s/quay\.io\/modh\/ray:[0-9]\+\.[0-9]\+\.[0-9]\+-py311-rocm[0-9]\+/quay.io\/modh\/ray:${{ github.event.inputs.new_ray_version }}-py311-rocm62/g" {} \; + fi + + if [ -n "${{ github.event.inputs.new_ray_version }}" ] && [ -n "${{ github.event.inputs.new_cuda_version_py312 }}" ]; then + find docs/ -name "*.rst" -exec sed -i "s/quay\.io\/modh\/ray:[0-9]\+\.[0-9]\+\.[0-9]\+-py312-cu[0-9]\+/quay.io\/modh\/ray:${{ github.event.inputs.new_ray_version }}-py312-${{ github.event.inputs.new_cuda_version_py312 }}/g" {} \; + fi + + - name: Update notebook files + run: | + # Update notebook files with new Ray version and image tags if provided + if [ -n "${{ github.event.inputs.new_ray_version }}" ] && [ -n "${{ github.event.inputs.new_cuda_version_py311 }}" ]; then + find demo-notebooks/ -name "*.ipynb" -exec sed -i "s/quay\.io\/modh\/ray:[0-9]\+\.[0-9]\+\.[0-9]\+-py311-cu[0-9]\+/quay.io\/modh\/ray:${{ github.event.inputs.new_ray_version }}-py311-${{ github.event.inputs.new_cuda_version_py311 }}/g" {} \; + fi + + if [ -n "${{ github.event.inputs.new_ray_version }}" ] && [ -n "${{ github.event.inputs.new_cuda_version_py312 }}" ]; then + find demo-notebooks/ -name "*.ipynb" -exec sed -i "s/quay\.io\/modh\/ray:[0-9]\+\.[0-9]\+\.[0-9]\+-py312-cu[0-9]\+/quay.io\/modh\/ray:${{ github.event.inputs.new_ray_version }}-py312-${{ github.event.inputs.new_cuda_version_py312 }}/g" {} \; + fi + + # Update notebook files with new Ray version only (for cases where CUDA version isn't specified) + if [ -n "${{ github.event.inputs.new_ray_version }}" ] && [ -z "${{ github.event.inputs.new_cuda_version_py311 }}" ] && [ -z "${{ github.event.inputs.new_cuda_version_py312 }}" ]; then + # Update Ray version in image tags while preserving existing CUDA versions + find demo-notebooks/ -name "*.ipynb" -exec sed -i "s/quay\.io\/modh\/ray:[0-9]\+\.[0-9]\+\.[0-9]\+-py311-cu[0-9]\+/quay.io\/modh\/ray:${{ github.event.inputs.new_ray_version }}-py311-cu121/g" {} \; + find demo-notebooks/ -name "*.ipynb" -exec sed -i "s/quay\.io\/modh\/ray:[0-9]\+\.[0-9]\+\.[0-9]\+-py312-cu[0-9]\+/quay.io\/modh\/ray:${{ github.event.inputs.new_ray_version }}-py312-cu128/g" {} \; + fi + + - name: Update YAML test files + run: | + # Update YAML files with new Ray version if provided + if [ -n "${{ github.event.inputs.new_ray_version }}" ]; then + find tests/ -name "*.yaml" -exec sed -i "s/rayVersion: [0-9]\+\.[0-9]\+\.[0-9]\+/rayVersion: ${{ github.event.inputs.new_ray_version }}/g" {} \; + fi + + # Update image tags in YAML files if Ray version and CUDA versions are provided + if [ -n "${{ github.event.inputs.new_ray_version }}" ] && [ -n "${{ github.event.inputs.new_cuda_version_py311 }}" ]; then + find tests/ -name "*.yaml" -exec sed -i "s/quay\.io\/modh\/ray:[0-9]\+\.[0-9]\+\.[0-9]\+-py311-cu[0-9]\+/quay.io\/modh\/ray:${{ github.event.inputs.new_ray_version }}-py311-${{ github.event.inputs.new_cuda_version_py311 }}/g" {} \; + fi + + - name: Update output YAML files + run: | + # Update output YAML files in demo-notebooks if Ray version and CUDA version are provided + if [ -n "${{ github.event.inputs.new_ray_version }}" ] && [ -n "${{ github.event.inputs.new_cuda_version_py311 }}" ]; then + find demo-notebooks/ -name "*.yaml" -exec sed -i "s/quay\.io\/modh\/ray:[0-9]\+\.[0-9]\+\.[0-9]\+-py311-cu[0-9]\+/quay.io\/modh\/ray:${{ github.event.inputs.new_ray_version }}-py311-${{ github.event.inputs.new_cuda_version_py311 }}/g" {} \; + fi + + - name: Validate updates + run: | + # Check if constants.py was updated correctly (only if Ray version was provided) + if [ -n "${{ github.event.inputs.new_ray_version }}" ]; then + if ! grep -q "RAY_VERSION = \"${{ github.event.inputs.new_ray_version }}\"" src/codeflare_sdk/common/utils/constants.py; then + echo "✗ Ray version not found in constants.py" + echo "Expected: RAY_VERSION = \"${{ github.event.inputs.new_ray_version }}\"" + echo "Found:" + grep "RAY_VERSION" src/codeflare_sdk/common/utils/constants.py || echo " (not found)" + exit 1 + fi + + # Check if pyproject.toml was updated correctly + if ! grep -q "ray = {version = \"${{ github.event.inputs.new_ray_version }}\"" pyproject.toml; then + echo "✗ Ray version not found in pyproject.toml" + echo "Expected: ray = {version = \"${{ github.event.inputs.new_ray_version }}\"" + echo "Found:" + grep "ray = " pyproject.toml || echo " (not found)" + exit 1 + fi + fi + + # Check if SDK version was updated correctly (only if SDK version was provided) + if [ -n "${{ github.event.inputs.new_sdk_version }}" ]; then + if ! grep -q "version = \"${{ github.event.inputs.new_sdk_version }}\"" pyproject.toml; then + echo "✗ SDK version not found in pyproject.toml" + echo "Expected: version = \"${{ github.event.inputs.new_sdk_version }}\"" + echo "Found:" + grep "version = " pyproject.toml || echo " (not found)" + exit 1 + fi + fi + + # Check if runtime images were updated (only if SHAs were provided) + if [ -n "${{ github.event.inputs.new_cuda_py311_sha }}" ]; then + if ! grep -q "quay.io/modh/ray@sha256:${{ github.event.inputs.new_cuda_py311_sha }}" src/codeflare_sdk/common/utils/constants.py; then + echo "✗ Python 3.11 runtime image not found" + echo "Expected: quay.io/modh/ray@sha256:${{ github.event.inputs.new_cuda_py311_sha }}" + echo "Found:" + grep "CUDA_PY311_RUNTIME_IMAGE" src/codeflare_sdk/common/utils/constants.py || echo " (not found)" + exit 1 + fi + fi + + if [ -n "${{ github.event.inputs.new_cuda_py312_sha }}" ]; then + if ! grep -q "quay.io/modh/ray@sha256:${{ github.event.inputs.new_cuda_py312_sha }}" src/codeflare_sdk/common/utils/constants.py; then + echo "✗ Python 3.12 runtime image not found" + echo "Expected: quay.io/modh/ray@sha256:${{ github.event.inputs.new_cuda_py312_sha }}" + echo "Found:" + grep "CUDA_PY312_RUNTIME_IMAGE" src/codeflare_sdk/common/utils/constants.py || echo " (not found)" + exit 1 + fi + fi + + - name: Check for changes + id: check-changes + run: | + if git diff --quiet; then + echo "has-changes=false" >> $GITHUB_OUTPUT + else + echo "has-changes=true" >> $GITHUB_OUTPUT + fi + + - name: Commit changes + if: steps.check-changes.outputs.has-changes == 'true' + run: | + git add . + + # Build commit message based on what was updated + COMMIT_MSG="Update Ray configuration" + + if [ -n "${{ github.event.inputs.new_ray_version }}" ]; then + COMMIT_MSG="$COMMIT_MSG + + - Updated Ray version to ${{ github.event.inputs.new_ray_version }} in constants.py and pyproject.toml" + fi + + if [ -n "${{ github.event.inputs.new_sdk_version }}" ]; then + COMMIT_MSG="$COMMIT_MSG + - Updated SDK version to ${{ github.event.inputs.new_sdk_version }} in pyproject.toml" + fi + + if [ -n "${{ github.event.inputs.new_cuda_py311_sha }}" ]; then + COMMIT_MSG="$COMMIT_MSG + - Updated Python 3.11 CUDA runtime image SHA" + fi + + if [ -n "${{ github.event.inputs.new_cuda_py312_sha }}" ]; then + COMMIT_MSG="$COMMIT_MSG + - Updated Python 3.12 CUDA runtime image SHA" + fi + + if [ -n "${{ github.event.inputs.new_cuda_version_py311 }}" ] || [ -n "${{ github.event.inputs.new_cuda_version_py312 }}" ]; then + COMMIT_MSG="$COMMIT_MSG + - Updated documentation and notebook files with new CUDA versions" + fi + + COMMIT_MSG="$COMMIT_MSG + + Parameters provided: + - Ray version: ${{ github.event.inputs.new_ray_version }} + - SDK version: ${{ github.event.inputs.new_sdk_version }} + - Python 3.11 CUDA version: ${{ github.event.inputs.new_cuda_version_py311 }} + - Python 3.12 CUDA version: ${{ github.event.inputs.new_cuda_version_py312 }} + - Python 3.11 runtime SHA: ${{ github.event.inputs.new_cuda_py311_sha }} + - Python 3.12 runtime SHA: ${{ github.event.inputs.new_cuda_py312_sha }}" + + git commit -m "$COMMIT_MSG" + + - name: Push changes + if: steps.check-changes.outputs.has-changes == 'true' + run: | + git push origin ${{ env.PR_BRANCH_NAME }} + + - name: Create Pull Request + if: steps.check-changes.outputs.has-changes == 'true' + run: | + # Get current versions for comparison + CURRENT_SDK_VERSION=$(grep -E "^version = " pyproject.toml | head -1 | sed 's/version = "\([^"]*\)"/\1/') + CURRENT_RAY_VERSION=$(grep "RAY_VERSION = " src/codeflare_sdk/common/utils/constants.py | sed 's/RAY_VERSION = "\([^"]*\)"/\1/') + + PR_TITLE="Pre-Release Version Updates" + + PR_BODY="## Pre-Release Version Updates + + This PR contains automated version updates for the upcoming release. + + ### Changes Made:" + + if [ -n "${{ github.event.inputs.new_sdk_version }}" ]; then + PR_BODY="$PR_BODY + - **CodeFlare SDK**: Bumped from \`v${CURRENT_SDK_VERSION}\` to \`v${{ github.event.inputs.new_sdk_version }}\`" + fi + + if [ -n "${{ github.event.inputs.new_ray_version }}" ]; then + PR_BODY="$PR_BODY + - **Ray**: Bumped from \`v${CURRENT_RAY_VERSION}\` to \`v${{ github.event.inputs.new_ray_version }}\`" + fi + + if [ -n "${{ github.event.inputs.new_cuda_py311_sha }}" ]; then + PR_BODY="$PR_BODY + - **Python 3.11 CUDA Runtime**: Updated to [\`${{ github.event.inputs.new_cuda_py311_sha }}\`](https://quay.io/repository/modh/ray/manifest/sha256:${{ github.event.inputs.new_cuda_py311_sha }})" + if [ -n "${{ github.event.inputs.new_cuda_version_py311 }}" ]; then + PR_BODY="$PR_BODY (CUDA \`${{ github.event.inputs.new_cuda_version_py311 }}\`)" + fi + fi + + if [ -n "${{ github.event.inputs.new_cuda_py312_sha }}" ]; then + PR_BODY="$PR_BODY + - **Python 3.12 CUDA Runtime**: Updated to [\`${{ github.event.inputs.new_cuda_py312_sha }}\`](https://quay.io/repository/modh/ray/manifest/sha256:${{ github.event.inputs.new_cuda_py312_sha }})" + if [ -n "${{ github.event.inputs.new_cuda_version_py312 }}" ]; then + PR_BODY="$PR_BODY (CUDA \`${{ github.event.inputs.new_cuda_version_py312 }}\`)" + fi + fi + + PR_BODY="$PR_BODY + + ### Testing: + Please run the test suite to ensure all changes work correctly: + \`\`\`bash + poetry install + poetry run pytest + \`\`\` + + ### Review Checklist: + - [ ] Verify version updates are correct + - [ ] Check that runtime image SHAs are valid + - [ ] Run tests to verify compatibility" + + # Create PR using GitHub CLI + gh pr create \ + --title "$PR_TITLE" \ + --body "$PR_BODY" \ + --base main \ + --head ${{ env.PR_BRANCH_NAME }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Comment on workflow run + if: steps.check-changes.outputs.has-changes == 'false' + run: | + echo "No changes were made. Please verify the input parameters." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 884632da..1d6371db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Thank you for your interest in contributing to the CodeFlare SDK! ### Prerequisites -- Python 3.9 +- Python 3.11 - [Poetry](https://python-poetry.org/) ### Setting Up Your Development Environment @@ -76,7 +76,7 @@ pytest -v src/codeflare_sdk ### Local e2e Testing -- Please follow the [e2e documentation](https://github.com/project-codeflare/codeflare-sdk/blob/main/docs/e2e.md) +- Please follow the [e2e documentation](https://github.com/project-codeflare/codeflare-sdk/blob/main/docs/sphinx/user-docs/e2e.rst) #### Code Coverage diff --git a/OWNERS b/OWNERS index f375f431..78bda8e8 100644 --- a/OWNERS +++ b/OWNERS @@ -1,23 +1,33 @@ approvers: - astefanutti - Bobbins228 + - CathalOConnorRH + - chipspeak - ChristianZaccaria - dimakis - Fiona-Waters - franciscojavierarceo - kpostoffice - - maxusmusti - - MichaelClifford + - kryanbeane + - laurafitzgerald + - pawelpaszki + - pmccarthy + - szaher - varshaprasad96 reviewers: - astefanutti - Bobbins228 + - CathalOConnorRH + - chipspeak - ChristianZaccaria - dimakis - Fiona-Waters - franciscojavierarceo - kpostoffice - - maxusmusti - - MichaelClifford + - kryanbeane + - laurafitzgerald + - pawelpaszki + - pmccarthy + - szaher - varshaprasad96 - Ygnas diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..4494dcd4 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,17 @@ +ignore: + - "**/*.ipynb" + - "demo-notebooks/**" + - "**/__init__.py" + +coverage: + precision: 2 + round: down + status: + project: + default: + target: auto + threshold: 2.5% + patch: + default: + target: 85% + threshold: 2.5% diff --git a/demo-notebooks/additional-demos/batch-inference/remote_offline_bi.ipynb b/demo-notebooks/additional-demos/batch-inference/remote_offline_bi.ipynb new file mode 100644 index 00000000..68b514c4 --- /dev/null +++ b/demo-notebooks/additional-demos/batch-inference/remote_offline_bi.ipynb @@ -0,0 +1,214 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Remote Offline Batch Inference with Ray Data & vLLM Example\n", + "\n", + "This notebook presumes:\n", + "- You have a Ray Cluster URL given to you to run workloads on\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from codeflare_sdk import RayJobClient\n", + "\n", + "# Setup Authentication Configuration\n", + "auth_token = \"XXXX\"\n", + "header = {\"Authorization\": f\"Bearer {auth_token}\"}" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# Gather the dashboard URL (provided by the creator of the RayCluster)\n", + "ray_dashboard = \"XXXX\" # Replace with the Ray dashboard URL\n", + "\n", + "# Initialize the RayJobClient\n", + "client = RayJobClient(address=ray_dashboard, headers=header, verify=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Simple Example Explanation\n", + "\n", + "With the RayJobClient instantiated, lets run some batch inference. The following code is stored in `simple_batch_inf.py`, and is used as the entrypoint for the RayJob.\n", + "\n", + "What this processor configuration does:\n", + "- Set up a vLLM engine with your model\n", + "- Configure some settings for GPU processing\n", + "- Defines batch processing parameters (8 requests per batch, 2 GPU workers)\n", + "\n", + "#### Model Source Configuration\n", + "\n", + "The `model_source` parameter supports several loading methods:\n", + "\n", + "* **Hugging Face Hub** (default): Use repository ID `model_source=\"meta-llama/Llama-2-7b-chat-hf\"`\n", + "* **Local Directory**: Use file path `model_source=\"/path/to/my/local/model\"`\n", + "* **Other Sources**: ModelScope via environment variables `VLLM_MODELSCOPE_DOWNLOADS_DIR`\n", + "\n", + "For complete model support and options, see the [official vLLM documentation](https://docs.vllm.ai/en/latest/models/supported_models.html).\n", + "\n", + "```python\n", + "import ray\n", + "from ray.data.llm import build_llm_processor, vLLMEngineProcessorConfig\n", + "\n", + "processor_config = vLLMEngineProcessorConfig(\n", + " model_source=\"replace-me\",\n", + " engine_kwargs=dict(\n", + " enable_lora=False,\n", + " dtype=\"half\",\n", + " max_model_len=1024,\n", + " ),\n", + " # Batch size: Larger batches increase throughput but reduce fault tolerance\n", + " # - Small batches (4-8): Better for fault tolerance and memory constraints\n", + " # - Large batches (16-32): Higher throughput, better GPU utilization\n", + " # - Choose based on your Ray Cluster size and memory availability\n", + " batch_size=8,\n", + " # Concurrency: Number of vLLM engine workers to spawn \n", + " # - Set to match your total GPU count for maximum utilization\n", + " # - Each worker gets assigned to a GPU automatically by Ray scheduler\n", + " # - Can use all GPUs across head and worker nodes\n", + " concurrency=2,\n", + ")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With the config defined, we can instantiate the processor. This enables batch inference by processing multiple requests through the vLLM engine, with two key steps:\n", + "- **Preprocess**: Converts each row into a structured chat format with system instructions and user queries, preparing the input for the LLM\n", + "- **Postprocess**: Extracts only the generated text from the model response, cleaning up the output\n", + "\n", + "The processor defines the pipeline that will be applied to each row in the dataset, enabling efficient batch processing through Ray Data's distributed execution framework.\n", + "\n", + "```python\n", + "processor = build_llm_processor(\n", + " processor_config,\n", + " preprocess=lambda row: dict(\n", + " messages=[\n", + " {\n", + " \"role\": \"system\",\n", + " \"content\": \"You are a calculator. Please only output the answer \"\n", + " \"of the given equation.\",\n", + " },\n", + " {\"role\": \"user\", \"content\": f\"{row['id']} ** 3 = ?\"},\n", + " ],\n", + " sampling_params=dict(\n", + " temperature=0.3,\n", + " max_tokens=20,\n", + " detokenize=False,\n", + " ),\n", + " ),\n", + " postprocess=lambda row: {\n", + " \"resp\": row[\"generated_text\"],\n", + " },\n", + ")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Running the Pipeline\n", + "Now we can run the batch inference pipeline on our data, it will:\n", + "- In the background, the processor will download the model into memory where vLLM serves it locally (on Ray Cluster) for use in inference\n", + "- Generate a sample Ray Dataset with 32 rows (0-31) to process\n", + "- Run the LLM processor on the dataset, triggering the preprocessing, inference, and postprocessing steps\n", + "- Execute the lazy pipeline and loads results into memory\n", + "- Iterate through all outputs and print each response \n", + "\n", + "```python\n", + "ds = ray.data.range(30)\n", + "ds = processor(ds)\n", + "ds = ds.materialize()\n", + "\n", + "for out in ds.take_all():\n", + " print(out)\n", + " print(\"==========\")\n", + "```\n", + "\n", + "### Job Submission\n", + "\n", + "Now we can submit this job against the Ray Cluster using the `RayJobClient` from earlier " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import tempfile\n", + "import shutil\n", + "\n", + "# Create a clean directory with ONLY your script\n", + "temp_dir = tempfile.mkdtemp()\n", + "shutil.copy(\"simple_batch_inf.py\", temp_dir)\n", + "\n", + "entrypoint_command = \"python simple_batch_inf.py\"\n", + "\n", + "submission_id = client.submit_job(\n", + " entrypoint=entrypoint_command,\n", + " runtime_env={\"working_dir\": temp_dir, \"pip\": \"requirements.txt\"},\n", + ")\n", + "\n", + "print(submission_id + \" successfully submitted\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get the job's status\n", + "client.get_job_status(submission_id)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get the job's logs\n", + "client.get_job_logs(submission_id)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/demo-notebooks/additional-demos/batch-inference/requirements.txt b/demo-notebooks/additional-demos/batch-inference/requirements.txt new file mode 100644 index 00000000..d9e8b73b --- /dev/null +++ b/demo-notebooks/additional-demos/batch-inference/requirements.txt @@ -0,0 +1,4 @@ +vllm +transformers +triton>=2.0.0 +torch>=2.0.0 diff --git a/demo-notebooks/additional-demos/batch-inference/simple_batch_inf.py b/demo-notebooks/additional-demos/batch-inference/simple_batch_inf.py new file mode 100644 index 00000000..c86ed15b --- /dev/null +++ b/demo-notebooks/additional-demos/batch-inference/simple_batch_inf.py @@ -0,0 +1,62 @@ +import ray +from ray.data.llm import build_llm_processor, vLLMEngineProcessorConfig + + +# 1. Construct a vLLM processor config. +processor_config = vLLMEngineProcessorConfig( + # The base model. + model_source="unsloth/Llama-3.2-1B-Instruct", + # vLLM engine config. + engine_kwargs=dict( + enable_lora=False, + # # Older GPUs (e.g. T4) don't support bfloat16. You should remove + # # this line if you're using later GPUs. + dtype="half", + # Reduce the model length to fit small GPUs. You should remove + # this line if you're using large GPUs. + max_model_len=1024, + ), + # The batch size used in Ray Data. + batch_size=8, + # Use one GPU in this example. + concurrency=1, + # If you save the LoRA adapter in S3, you can set the following path. + # dynamic_lora_loading_path="s3://your-lora-bucket/", +) + +# 2. Construct a processor using the processor config. +processor = build_llm_processor( + processor_config, + preprocess=lambda row: dict( + # Remove the LoRA model specification + messages=[ + { + "role": "system", + "content": "You are a calculator. Please only output the answer " + "of the given equation.", + }, + {"role": "user", "content": f"{row['id']} ** 3 = ?"}, + ], + sampling_params=dict( + temperature=0.3, + max_tokens=20, + detokenize=False, + ), + ), + postprocess=lambda row: { + "resp": row["generated_text"], + }, +) + +# 3. Synthesize a dataset with 32 rows. +ds = ray.data.range(32) +# 4. Apply the processor to the dataset. Note that this line won't kick off +# anything because processor is execution lazily. +ds = processor(ds) +# Materialization kicks off the pipeline execution. +ds = ds.materialize() + +# 5. Print all outputs. +for out in ds.take_all(): + print(out) + print("==========") diff --git a/demo-notebooks/additional-demos/hf_interactive.ipynb b/demo-notebooks/additional-demos/hf_interactive.ipynb index d75d96ec..9b32ab2e 100644 --- a/demo-notebooks/additional-demos/hf_interactive.ipynb +++ b/demo-notebooks/additional-demos/hf_interactive.ipynb @@ -70,8 +70,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -115,7 +115,7 @@ "metadata": {}, "outputs": [], "source": [ - "cluster.up()" + "cluster.apply()" ] }, { diff --git a/demo-notebooks/additional-demos/local_interactive.ipynb b/demo-notebooks/additional-demos/local_interactive.ipynb index 09cb9b89..257c6c1b 100644 --- a/demo-notebooks/additional-demos/local_interactive.ipynb +++ b/demo-notebooks/additional-demos/local_interactive.ipynb @@ -37,8 +37,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -80,7 +80,7 @@ }, "outputs": [], "source": [ - "cluster.up()" + "cluster.apply()" ] }, { diff --git a/demo-notebooks/additional-demos/ray_job_client.ipynb b/demo-notebooks/additional-demos/ray_job_client.ipynb index 31c5793e..42d3faa0 100644 --- a/demo-notebooks/additional-demos/ray_job_client.ipynb +++ b/demo-notebooks/additional-demos/ray_job_client.ipynb @@ -43,8 +43,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -79,7 +79,7 @@ "outputs": [], "source": [ "# Bring up the cluster\n", - "cluster.up()\n", + "cluster.apply()\n", "cluster.wait_ready()" ] }, diff --git a/demo-notebooks/additional-demos/remote_ray_job_client.ipynb b/demo-notebooks/additional-demos/remote_ray_job_client.ipynb new file mode 100644 index 00000000..b2be6826 --- /dev/null +++ b/demo-notebooks/additional-demos/remote_ray_job_client.ipynb @@ -0,0 +1,103 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Submit a training job remotely to Ray Dashboard protected by oAuth.\n", + "This notebook will demonstrate how to submit Ray jobs to an existing Raycluster, using the CodeFlare SDK.\n", + "\n", + "### Requirements\n", + "* Ray Cluster running in OpenShift protected by oAuth.\n", + "* The Ray Dashboard URL for the Ray Cluster.\n", + "* An OpenShift authorization token with permissions to access the Route.\n", + "* A training job, defined in python, within the working directory.\n", + "* A requirements.txt or equivalent file containing any additional packages to install onto the Ray images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Import dependencies from codeflare-sdk\n", + "from codeflare_sdk import RayJobClient" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Setup Authentication Configuration \n", + "auth_token = \"XXXX\" # Replace with the actual token\n", + "header = {\n", + " 'Authorization': f'Bearer {auth_token}'\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Gather the dashboard URL (provided by the creator of the RayCluster)\n", + "ray_dashboard = \"XXXX\" # Replace with the Ray dashboard URL" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#Initialize the RayJobClient\n", + "client = RayJobClient(address=ray_dashboard, headers=header, verify=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Submit a job using the RayJobClient\n", + "entrypoint_command = \"python XXXX\" # Replace with the training script name\n", + "submission_id = client.submit_job(\n", + " entrypoint=entrypoint_command,\n", + " runtime_env={\"working_dir\": \"./\",\"pip\": \"requirements.txt\"},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get the job's status\n", + "client.get_job_status(submission_id)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get the job's logs\n", + "client.get_job_logs(submission_id)" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/demo-notebooks/guided-demos/0_basic_ray.ipynb b/demo-notebooks/guided-demos/0_basic_ray.ipynb index be05130e..7bc69afa 100644 --- a/demo-notebooks/guided-demos/0_basic_ray.ipynb +++ b/demo-notebooks/guided-demos/0_basic_ray.ipynb @@ -49,8 +49,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -99,7 +99,7 @@ "outputs": [], "source": [ "# Bring up the cluster\n", - "cluster.up()" + "cluster.apply()" ] }, { diff --git a/demo-notebooks/guided-demos/1_cluster_job_client.ipynb b/demo-notebooks/guided-demos/1_cluster_job_client.ipynb index 8db650dd..2f042a6d 100644 --- a/demo-notebooks/guided-demos/1_cluster_job_client.ipynb +++ b/demo-notebooks/guided-demos/1_cluster_job_client.ipynb @@ -43,8 +43,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -90,7 +90,7 @@ "outputs": [], "source": [ "# Bring up the cluster\n", - "cluster.up()\n", + "cluster.apply()\n", "cluster.wait_ready()" ] }, diff --git a/demo-notebooks/guided-demos/2_basic_interactive.ipynb b/demo-notebooks/guided-demos/2_basic_interactive.ipynb index 5528d04b..683ec236 100644 --- a/demo-notebooks/guided-demos/2_basic_interactive.ipynb +++ b/demo-notebooks/guided-demos/2_basic_interactive.ipynb @@ -46,8 +46,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -97,7 +97,7 @@ "outputs": [], "source": [ "# Bring up the cluster\n", - "cluster.up()\n", + "cluster.apply()\n", "cluster.wait_ready()" ] }, diff --git a/demo-notebooks/guided-demos/3_widget_example.ipynb b/demo-notebooks/guided-demos/3_widget_example.ipynb index 243c75ec..8b70e1da 100644 --- a/demo-notebooks/guided-demos/3_widget_example.ipynb +++ b/demo-notebooks/guided-demos/3_widget_example.ipynb @@ -49,8 +49,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] diff --git a/demo-notebooks/guided-demos/4_rayjob_existing_cluster.ipynb b/demo-notebooks/guided-demos/4_rayjob_existing_cluster.ipynb new file mode 100644 index 00000000..c0737db0 --- /dev/null +++ b/demo-notebooks/guided-demos/4_rayjob_existing_cluster.ipynb @@ -0,0 +1,207 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9259e514", + "metadata": {}, + "source": [ + "# Submitting RayJobs against an existing RayCluster\n", + "\n", + "In this notebook, we will go through the basics of using the SDK to:\n", + " * Spin up a Ray cluster with our desired resources\n", + " * Verify the status of this cluster\n", + " * Submit a RayJob against that cluster\n", + " * Verify the status of this job" + ] + }, + { + "cell_type": "markdown", + "id": "18136ea7", + "metadata": {}, + "source": [ + "## Creating the RayCluster" + ] + }, + { + "cell_type": "markdown", + "id": "a1c2545d", + "metadata": {}, + "source": [ + "First, we'll need to import the relevant CodeFlare SDK packages. You can do this by executing the below cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51e18292", + "metadata": {}, + "outputs": [], + "source": [ + "from codeflare_sdk import Cluster, ClusterConfiguration, RayJob" + ] + }, + { + "cell_type": "markdown", + "id": "649c5911", + "metadata": {}, + "source": [ + "Run the below `oc login` command using your Token and Server URL. Ensure the command is prepended by `!` and not `%`. This will work when running both locally and within RHOAI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc364888", + "metadata": {}, + "outputs": [], + "source": [ + "!oc login --token= --server=" + ] + }, + { + "cell_type": "markdown", + "id": "5581eca9", + "metadata": {}, + "source": [ + "Next we'll need to initalize our RayCluster and apply it. You can do this be executing the below cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3094c60a", + "metadata": {}, + "outputs": [], + "source": [ + "cluster = Cluster(ClusterConfiguration(\n", + " name='rayjob-cluster',\n", + " head_extended_resource_requests={'nvidia.com/gpu':0},\n", + " worker_extended_resource_requests={'nvidia.com/gpu':0},\n", + " num_workers=2,\n", + " worker_cpu_requests=1,\n", + " worker_cpu_limits=1,\n", + " worker_memory_requests=4,\n", + " worker_memory_limits=4,\n", + "\n", + "))\n", + "\n", + "cluster.apply()" + ] + }, + { + "cell_type": "markdown", + "id": "f3612de2", + "metadata": {}, + "source": [ + "We can check the status of our cluster by executing the below cell. If it's not up immediately, run the cell a few more times until you see that it's in a 'running' state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96d92f93", + "metadata": {}, + "outputs": [], + "source": [ + "cluster.status()" + ] + }, + { + "cell_type": "markdown", + "id": "a0e2a650", + "metadata": {}, + "source": [ + "## Submitting the RayJob" + ] + }, + { + "cell_type": "markdown", + "id": "4cf03419", + "metadata": {}, + "source": [ + "Now we can create the RayJob that we want to submit against the running cluster. The process is quite similar to how we initialize and apply the cluster. \n", + "In this context, we need to use the `cluster_name` variable to point it to our existing cluster.\n", + "\n", + "For the sake of demonstration, the job we'll submit via the `entrypoint` is a single python command. In standard practice this would be pointed to a python training script.\n", + "\n", + "We'll then call the `submit()` function to run the job against our cluster.\n", + "\n", + "You can run the below cell to achieve this." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94edca70", + "metadata": {}, + "outputs": [], + "source": [ + "rayjob = RayJob(\n", + " job_name=\"sdk-test-job\",\n", + " cluster_name=\"rayjob-cluster\",\n", + " namespace=\"your-namespace\",\n", + " entrypoint=\"python -c 'import time; time.sleep(20)'\",\n", + ")\n", + "\n", + "rayjob.submit()" + ] + }, + { + "cell_type": "markdown", + "id": "30a8899a", + "metadata": {}, + "source": [ + "We can observe the status of the RayJob in the same way as the RayCluster by invoking the `status()` function via the below cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3283b09c", + "metadata": {}, + "outputs": [], + "source": [ + "rayjob.status()" + ] + }, + { + "cell_type": "markdown", + "id": "9f3c9c9f", + "metadata": {}, + "source": [ + "This function will output different tables based on the RayJob's current status. You can re-run the cell multiple times to observe the changes as you need to. Once you've observed that the job has been completed, you can shut down the cluster we created earlier by executing the below cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b11e379", + "metadata": {}, + "outputs": [], + "source": [ + "cluster.down()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demo-notebooks/guided-demos/5_submit_rayjob_cr.ipynb b/demo-notebooks/guided-demos/5_submit_rayjob_cr.ipynb new file mode 100644 index 00000000..1d9630b7 --- /dev/null +++ b/demo-notebooks/guided-demos/5_submit_rayjob_cr.ipynb @@ -0,0 +1,144 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9259e514", + "metadata": {}, + "source": [ + "# Submitting a RayJob CR\n", + "\n", + "In this notebook, we will go through the basics of using the SDK to:\n", + " * Define a RayCluster configuration\n", + " * Use this configuration alongside a RayJob definition\n", + " * Submit the RayJob, and allow Kuberay Operator to lifecycle the RayCluster for the RayJob" + ] + }, + { + "cell_type": "markdown", + "id": "18136ea7", + "metadata": {}, + "source": [ + "## Defining and Submitting the RayJob\n", + "First, we'll need to import the relevant CodeFlare SDK packages. You can do this by executing the below cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51e18292", + "metadata": {}, + "outputs": [], + "source": [ + "from codeflare_sdk import RayJob, ManagedClusterConfig" + ] + }, + { + "cell_type": "markdown", + "id": "649c5911", + "metadata": {}, + "source": [ + "Run the below `oc login` command using your Token and Server URL. Ensure the command is prepended by `!` and not `%`. This will work when running both locally and within RHOAI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc364888", + "metadata": {}, + "outputs": [], + "source": [ + "!oc login --token= --server=" + ] + }, + { + "cell_type": "markdown", + "id": "5581eca9", + "metadata": {}, + "source": [ + "Next we'll need to define the ManagedClusterConfig. Kuberay will use this to spin up a short-lived RayCluster that will only exist as long as the job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3094c60a", + "metadata": {}, + "outputs": [], + "source": [ + "cluster_config = ManagedClusterConfig(\n", + " num_workers=2,\n", + " worker_cpu_requests=1,\n", + " worker_cpu_limits=1,\n", + " worker_memory_requests=4,\n", + " worker_memory_limits=4,\n", + " head_accelerators={'nvidia.com/gpu': 0},\n", + " worker_accelerators={'nvidia.com/gpu': 0},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "02a2b32b", + "metadata": {}, + "source": [ + "Lastly we can pass the ManagedClusterConfig into the RayJob and submit it. You do not need to worry about tearing down the cluster when the job has completed, that is handled for you!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e905ccea", + "metadata": {}, + "outputs": [], + "source": [ + "job = RayJob(\n", + " job_name=\"demo-rayjob\",\n", + " entrypoint=\"python -c 'print(\\\"Hello from RayJob!\\\")'\",\n", + " cluster_config=cluster_config,\n", + " namespace=\"your-namespace\"\n", + ")\n", + "\n", + "job.submit()" + ] + }, + { + "cell_type": "markdown", + "id": "f3612de2", + "metadata": {}, + "source": [ + "We can check the status of our job by executing the below cell. The status may appear as `unknown` for a time while the RayCluster spins up." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96d92f93", + "metadata": {}, + "outputs": [], + "source": [ + "job.status()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demo-notebooks/guided-demos/notebook-ex-outputs/0_basic_ray.ipynb b/demo-notebooks/guided-demos/notebook-ex-outputs/0_basic_ray.ipynb index 3e6dc193..49f7f687 100644 --- a/demo-notebooks/guided-demos/notebook-ex-outputs/0_basic_ray.ipynb +++ b/demo-notebooks/guided-demos/notebook-ex-outputs/0_basic_ray.ipynb @@ -49,8 +49,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -95,7 +95,7 @@ "outputs": [], "source": [ "# Bring up the cluster\n", - "cluster.up()" + "cluster.apply()" ] }, { diff --git a/demo-notebooks/guided-demos/notebook-ex-outputs/1_cluster_job_client.ipynb b/demo-notebooks/guided-demos/notebook-ex-outputs/1_cluster_job_client.ipynb index e79d47e7..913fb919 100644 --- a/demo-notebooks/guided-demos/notebook-ex-outputs/1_cluster_job_client.ipynb +++ b/demo-notebooks/guided-demos/notebook-ex-outputs/1_cluster_job_client.ipynb @@ -43,8 +43,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -79,7 +79,7 @@ "outputs": [], "source": [ "# Bring up the cluster\n", - "cluster.up()\n", + "cluster.apply()\n", "cluster.wait_ready()" ] }, diff --git a/demo-notebooks/guided-demos/notebook-ex-outputs/2_basic_interactive.ipynb b/demo-notebooks/guided-demos/notebook-ex-outputs/2_basic_interactive.ipynb index f6417521..9c816c53 100644 --- a/demo-notebooks/guided-demos/notebook-ex-outputs/2_basic_interactive.ipynb +++ b/demo-notebooks/guided-demos/notebook-ex-outputs/2_basic_interactive.ipynb @@ -46,8 +46,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -85,7 +85,7 @@ "outputs": [], "source": [ "# Bring up the cluster\n", - "cluster.up()\n", + "cluster.apply()\n", "cluster.wait_ready()" ] }, diff --git a/demo-notebooks/guided-demos/notebook-ex-outputs/interactivetest.yaml b/demo-notebooks/guided-demos/notebook-ex-outputs/interactivetest.yaml index fd6500a7..443da33c 100644 --- a/demo-notebooks/guided-demos/notebook-ex-outputs/interactivetest.yaml +++ b/demo-notebooks/guided-demos/notebook-ex-outputs/interactivetest.yaml @@ -81,7 +81,7 @@ spec: value: /home/ray/workspace/tls/server.key - name: RAY_TLS_CA_CERT value: /home/ray/workspace/tls/ca.crt - image: quay.io/modh/ray:2.35.0-py39-cu121 + image: quay.io/modh/ray:2.47.1-py311-cu121 imagePullPolicy: Always lifecycle: preStop: @@ -108,7 +108,7 @@ spec: memory: 8G nvidia.com/gpu: 0 imagePullSecrets: [] - rayVersion: 2.1.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-interactivetest maxReplicas: 2 @@ -147,7 +147,7 @@ spec: value: /home/ray/workspace/tls/server.key - name: RAY_TLS_CA_CERT value: /home/ray/workspace/tls/ca.crt - image: quay.io/modh/ray:2.35.0-py39-cu121 + image: quay.io/modh/ray:2.47.1-py311-cu121 lifecycle: preStop: exec: diff --git a/demo-notebooks/guided-demos/notebook-ex-outputs/jobtest.yaml b/demo-notebooks/guided-demos/notebook-ex-outputs/jobtest.yaml index a33a9cf5..5d5b0b0e 100644 --- a/demo-notebooks/guided-demos/notebook-ex-outputs/jobtest.yaml +++ b/demo-notebooks/guided-demos/notebook-ex-outputs/jobtest.yaml @@ -70,7 +70,7 @@ spec: value: /home/ray/workspace/tls/server.key - name: RAY_TLS_CA_CERT value: /home/ray/workspace/tls/ca.crt - image: quay.io/modh/ray:2.35.0-py39-cu121 + image: quay.io/modh/ray:2.47.1-py311-cu121 imagePullPolicy: Always lifecycle: preStop: @@ -97,7 +97,7 @@ spec: memory: 8G nvidia.com/gpu: 0 imagePullSecrets: [] - rayVersion: 2.1.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-jobtest maxReplicas: 2 @@ -127,7 +127,7 @@ spec: value: /home/ray/workspace/tls/server.key - name: RAY_TLS_CA_CERT value: /home/ray/workspace/tls/ca.crt - image: quay.io/modh/ray:2.35.0-py39-cu121 + image: quay.io/modh/ray:2.47.1-py311-cu121 lifecycle: preStop: exec: diff --git a/demo-notebooks/guided-demos/notebook-ex-outputs/raytest.yaml b/demo-notebooks/guided-demos/notebook-ex-outputs/raytest.yaml index 151d2e28..81796687 100644 --- a/demo-notebooks/guided-demos/notebook-ex-outputs/raytest.yaml +++ b/demo-notebooks/guided-demos/notebook-ex-outputs/raytest.yaml @@ -70,7 +70,7 @@ spec: value: /home/ray/workspace/tls/server.key - name: RAY_TLS_CA_CERT value: /home/ray/workspace/tls/ca.crt - image: quay.io/modh/ray:2.35.0-py39-cu121 + image: quay.io/modh/ray:2.47.1-py311-cu121 imagePullPolicy: Always lifecycle: preStop: @@ -97,7 +97,7 @@ spec: memory: 8G nvidia.com/gpu: 0 imagePullSecrets: [] - rayVersion: 2.1.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-raytest maxReplicas: 2 @@ -127,7 +127,7 @@ spec: value: /home/ray/workspace/tls/server.key - name: RAY_TLS_CA_CERT value: /home/ray/workspace/tls/ca.crt - image: quay.io/modh/ray:2.35.0-py39-cu121 + image: quay.io/modh/ray:2.47.1-py311-cu121 lifecycle: preStop: exec: diff --git a/demo-notebooks/guided-demos/preview_nbs/0_basic_ray.ipynb b/demo-notebooks/guided-demos/preview_nbs/0_basic_ray.ipynb index 3e6dc193..49f7f687 100644 --- a/demo-notebooks/guided-demos/preview_nbs/0_basic_ray.ipynb +++ b/demo-notebooks/guided-demos/preview_nbs/0_basic_ray.ipynb @@ -49,8 +49,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -95,7 +95,7 @@ "outputs": [], "source": [ "# Bring up the cluster\n", - "cluster.up()" + "cluster.apply()" ] }, { diff --git a/demo-notebooks/guided-demos/preview_nbs/1_cluster_job_client.ipynb b/demo-notebooks/guided-demos/preview_nbs/1_cluster_job_client.ipynb index 40195d64..3c7b7876 100644 --- a/demo-notebooks/guided-demos/preview_nbs/1_cluster_job_client.ipynb +++ b/demo-notebooks/guided-demos/preview_nbs/1_cluster_job_client.ipynb @@ -43,8 +43,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -79,7 +79,7 @@ "outputs": [], "source": [ "# Bring up the cluster\n", - "cluster.up()\n", + "cluster.apply()\n", "cluster.wait_ready()" ] }, diff --git a/demo-notebooks/guided-demos/preview_nbs/2_basic_interactive.ipynb b/demo-notebooks/guided-demos/preview_nbs/2_basic_interactive.ipynb index 8838a5ba..1de3fc9c 100644 --- a/demo-notebooks/guided-demos/preview_nbs/2_basic_interactive.ipynb +++ b/demo-notebooks/guided-demos/preview_nbs/2_basic_interactive.ipynb @@ -46,8 +46,8 @@ "\n", "NOTE: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version:\n", "\n", - "- For Python 3.9: 'quay.io/modh/ray:2.35.0-py39-cu121'\n", - "- For Python 3.11: 'quay.io/modh/ray:2.35.0-py311-cu121'\n", + "- For Python 3.11: 'quay.io/modh/ray:2.47.1-py311-cu121'\n", + "- For Python 3.12: 'quay.io/modh/ray:2.47.1-py312-cu128'\n", "\n", "If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default." ] @@ -85,7 +85,7 @@ "outputs": [], "source": [ "# Bring up the cluster\n", - "cluster.up()\n", + "cluster.apply()\n", "cluster.wait_ready()" ] }, diff --git a/demo-notebooks/guided-demos/preview_nbs/4_rayjob_existing_cluster.ipynb b/demo-notebooks/guided-demos/preview_nbs/4_rayjob_existing_cluster.ipynb new file mode 100644 index 00000000..5348099c --- /dev/null +++ b/demo-notebooks/guided-demos/preview_nbs/4_rayjob_existing_cluster.ipynb @@ -0,0 +1,212 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9259e514", + "metadata": {}, + "source": [ + "# Submitting RayJobs against an existing RayCluster\n", + "\n", + "In this notebook, we will go through the basics of using the SDK to:\n", + " * Spin up a Ray cluster with our desired resources\n", + " * Verify the status of this cluster\n", + " * Submit a RayJob against that cluster\n", + " * Verify the status of this job" + ] + }, + { + "cell_type": "markdown", + "id": "18136ea7", + "metadata": {}, + "source": [ + "## Creating the RayCluster" + ] + }, + { + "cell_type": "markdown", + "id": "a1c2545d", + "metadata": {}, + "source": [ + "First, we'll need to import the relevant CodeFlare SDK packages. You can do this by executing the below cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51e18292", + "metadata": {}, + "outputs": [], + "source": [ + "from codeflare_sdk import Cluster, ClusterConfiguration, RayJob, TokenAuthentication" + ] + }, + { + "cell_type": "markdown", + "id": "649c5911", + "metadata": {}, + "source": [ + "Execute the below cell to authenticate the notebook via OpenShift." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc364888", + "metadata": {}, + "outputs": [], + "source": [ + "auth = TokenAuthentication(\n", + " token = \"XXXXX\",\n", + " server = \"XXXXX\",\n", + " skip_tls=False\n", + ")\n", + "auth.login()" + ] + }, + { + "cell_type": "markdown", + "id": "5581eca9", + "metadata": {}, + "source": [ + "Next we'll need to initalize our RayCluster and apply it. You can do this be executing the below cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3094c60a", + "metadata": {}, + "outputs": [], + "source": [ + "cluster = Cluster(ClusterConfiguration(\n", + " name='rayjob-cluster',\n", + " head_extended_resource_requests={'nvidia.com/gpu':0},\n", + " worker_extended_resource_requests={'nvidia.com/gpu':0},\n", + " num_workers=2,\n", + " worker_cpu_requests=1,\n", + " worker_cpu_limits=1,\n", + " worker_memory_requests=4,\n", + " worker_memory_limits=4,\n", + "\n", + "))\n", + "\n", + "cluster.apply()" + ] + }, + { + "cell_type": "markdown", + "id": "f3612de2", + "metadata": {}, + "source": [ + "We can check the status of our cluster by executing the below cell. If it's not up immediately, run the cell a few more times until you see that it's in a 'running' state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96d92f93", + "metadata": {}, + "outputs": [], + "source": [ + "cluster.status()" + ] + }, + { + "cell_type": "markdown", + "id": "a0e2a650", + "metadata": {}, + "source": [ + "## Creating and Submitting the RayJob" + ] + }, + { + "cell_type": "markdown", + "id": "4cf03419", + "metadata": {}, + "source": [ + "Now we can create the RayJob that we want to submit against the running cluster. The process is quite similar to how we initialize and apply the cluster. \n", + "In this context, we need to use the `cluster_name` variable to point it to our existing cluster.\n", + "\n", + "For the sake of demonstration, the job we'll submit via the `entrypoint` is a single python command. In standard practice this would be pointed to a python training script.\n", + "\n", + "We'll then call the `submit()` function to run the job against our cluster.\n", + "\n", + "You can run the below cell to achieve this." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94edca70", + "metadata": {}, + "outputs": [], + "source": [ + "rayjob = RayJob(\n", + " job_name=\"sdk-test-job\",\n", + " cluster_name=\"rayjob-cluster\",\n", + " namespace=\"rhods-notebooks\",\n", + " entrypoint=\"python -c 'import time; time.sleep(20)'\",\n", + ")\n", + "\n", + "rayjob.submit()" + ] + }, + { + "cell_type": "markdown", + "id": "30a8899a", + "metadata": {}, + "source": [ + "We can observe the status of the RayJob in the same way as the RayCluster by invoking the `submit()` function via the below cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3283b09c", + "metadata": {}, + "outputs": [], + "source": [ + "rayjob.submit()" + ] + }, + { + "cell_type": "markdown", + "id": "9f3c9c9f", + "metadata": {}, + "source": [ + "This function will output different tables based on the RayJob's current status. You can re-run the cell multiple times to observe the changes as you need to. Once you've observed that the job has been completed, you can shut down the cluster we created earlier by executing the below cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b11e379", + "metadata": {}, + "outputs": [], + "source": [ + "cluster.down()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/designs/History/CodeFlareSDK_Design_Doc.md b/docs/designs/History/CodeFlareSDK_Design_Doc.md index 4992406b..c7cb26fc 100644 --- a/docs/designs/History/CodeFlareSDK_Design_Doc.md +++ b/docs/designs/History/CodeFlareSDK_Design_Doc.md @@ -42,7 +42,7 @@ Users can customize their AppWrapper by passing their desired parameters to `Clu Our aim is to simplify the process of generating valid AppWrappers for RayClusters, so we will strive to find the appropriate balance between ease of use and exposing all possible AppWrapper parameters. And we will find this balance through user feedback. -With a valid AppWrapper, we will use the Kubernetes python client to apply the AppWrapper to our Kubernetes cluster via a call to `cluster.up()` +With a valid AppWrapper, we will use the Kubernetes python client to apply the AppWrapper to our Kubernetes cluster via a call to `cluster.apply()` We will also use the Kubernetes python client to get information about both the RayCluster and AppWrapper custom resources to monitor the status of our Framework Cluster via `cluster.status()` and `cluster.details()`. diff --git a/docs/sphinx/user-docs/cluster-configuration.rst b/docs/sphinx/user-docs/cluster-configuration.rst index dc3f2cf4..2cf5b213 100644 --- a/docs/sphinx/user-docs/cluster-configuration.rst +++ b/docs/sphinx/user-docs/cluster-configuration.rst @@ -26,53 +26,132 @@ requirements for creating the Ray Cluster. worker_memory_limits=2, # Default 2 # image="", # Optional Field labels={"exampleLabel": "example", "secondLabel": "example"}, + annotations={"key1":"value1", "key2":"value2"}, + volumes=[], # See Custom Volumes/Volume Mounts + volume_mounts=[], # See Custom Volumes/Volume Mounts )) .. note:: The default images used by the CodeFlare SDK for creating a RayCluster resource depend on the installed Python version: - - For Python 3.9: `quay.io/modh/ray:2.35.0-py39-cu121` - - For Python 3.11: `quay.io/modh/ray:2.35.0-py311-cu121` + - For Python 3.11: `quay.io/modh/ray:2.47.1-py311-cu121` If you prefer to use a custom Ray image that better suits your needs, you can specify it in the image field to override the default. If you are using ROCm compatible GPUs you - can use `quay.io/modh/ray:2.35.0-py39-rocm61`. You can also find + can use `quay.io/modh/ray:2.47.1-py311-rocm62`. You can also find documentation on building a custom image `here `__. +Ray Usage Statistics +------------------- + +By default, Ray usage statistics collection is **disabled** in Ray Clusters created with the Codeflare SDK. This prevents statistics from being captured and sent externally. If you want to enable usage statistics collection, you can simply set the ``enable_usage_stats`` parameter to ``True`` in your cluster configuration: + +.. code:: python + + from codeflare_sdk import Cluster, ClusterConfiguration + + cluster = Cluster(ClusterConfiguration( + name='ray-example', + namespace='default', + enable_usage_stats=True + )) + +This will automatically set the ``RAY_USAGE_STATS_ENABLED`` environment variable to ``1`` for all Ray pods in the cluster. If you do not set this parameter, usage statistics will remain disabled (``RAY_USAGE_STATS_ENABLED=0``). + The ``labels={"exampleLabel": "example"}`` parameter can be used to apply additional labels to the RayCluster resource. -After creating their ``cluster``, a user can call ``cluster.up()`` and +After creating their ``cluster``, a user can call ``cluster.apply()`` and ``cluster.down()`` to respectively create or remove the Ray Cluster. -Deprecating Parameters ----------------------- +Custom Volumes/Volume Mounts +---------------------------- +| To add custom Volumes and Volume Mounts to your Ray Cluster you need to create two lists ``volumes`` and ``volume_mounts``. The lists consist of ``V1Volume`` and ``V1VolumeMount`` objects respectively. +| Populating these parameters will create Volumes and Volume Mounts for the head and each worker pod. + +.. code:: python + + from kubernetes.client import V1Volume, V1VolumeMount, V1EmptyDirVolumeSource, V1ConfigMapVolumeSource, V1KeyToPath, V1SecretVolumeSource + # In this example we are using the Config Map, EmptyDir and Secret Volume types + volume_mounts_list = [ + V1VolumeMount( + mount_path="/home/ray/test1", + name = "test" + ), + V1VolumeMount( + mount_path = "/home/ray/test2", + name = "test2", + ), + V1VolumeMount( + mount_path = "/home/ray/test3", + name = "test3", + ) + ] -The following parameters of the ``ClusterConfiguration`` are being -deprecated. + volumes_list = [ + V1Volume( + name="test", + empty_dir=V1EmptyDirVolumeSource(size_limit="2Gi"), + ), + V1Volume( + name="test2", + config_map=V1ConfigMapVolumeSource( + name="test-config-map", + items=[V1KeyToPath(key="test", path="data.txt")] + ) + ), + V1Volume( + name="test3", + secret=V1SecretVolumeSource( + secret_name="test-secret" + ) + ) + ] + +| For more information on creating Volumes and Volume Mounts with Python check out the Python Kubernetes docs (`Volumes `__, `Volume Mounts `__). +| You can also find further information on Volumes and Volume Mounts by visiting the Kubernetes `documentation `__. + +GCS Fault Tolerance +------------------ +By default, the state of the Ray cluster is transient to the head Pod. Whatever triggers a restart of the head Pod results in losing that state, including Ray Cluster history. To make Ray cluster state persistent you can enable Global Control Service (GCS) fault tolerance with an external Redis storage. + +To configure GCS fault tolerance you need to set the following parameters: .. list-table:: :header-rows: 1 :widths: auto - * - Deprecated Parameter - - Replaced By - * - ``head_cpus`` - - ``head_cpu_requests``, ``head_cpu_limits`` - * - ``head_memory`` - - ``head_memory_requests``, ``head_memory_limits`` - * - ``min_cpus`` - - ``worker_cpu_requests`` - * - ``max_cpus`` - - ``worker_cpu_limits`` - * - ``min_memory`` - - ``worker_memory_requests`` - * - ``max_memory`` - - ``worker_memory_limits`` - * - ``head_gpus`` - - ``head_extended_resource_requests`` - * - ``num_gpus`` - - ``worker_extended_resource_requests`` + * - Parameter + - Description + * - ``enable_gcs_ft`` + - Boolean to enable GCS fault tolerance + * - ``redis_address`` + - Address of the external Redis service, ex: "redis:6379" + * - ``redis_password_secret`` + - Dictionary with 'name' and 'key' fields specifying the Kubernetes secret for Redis password + * - ``external_storage_namespace`` + - Custom storage namespace for GCS fault tolerance (by default, KubeRay sets it to the RayCluster's UID) + +Example configuration: + +.. code:: python + + from codeflare_sdk import Cluster, ClusterConfiguration + + cluster = Cluster(ClusterConfiguration( + name='ray-cluster-with-persistence', + num_workers=2, + enable_gcs_ft=True, + redis_address="redis:6379", + redis_password_secret={ + "name": "redis-password-secret", + "key": "password" + }, + # external_storage_namespace="my-custom-namespace" # Optional: Custom namespace for GCS data in Redis + )) + +.. note:: + You need to have a Redis instance deployed in your Kubernetes cluster before using this feature. diff --git a/docs/sphinx/user-docs/e2e.rst b/docs/sphinx/user-docs/e2e.rst index 846536f1..6f3d1462 100644 --- a/docs/sphinx/user-docs/e2e.rst +++ b/docs/sphinx/user-docs/e2e.rst @@ -4,7 +4,7 @@ Running e2e tests locally Pre-requisites ^^^^^^^^^^^^^^ -- We recommend using Python 3.9, along with Poetry. +- We recommend using Python 3.11, along with Poetry. On KinD clusters ---------------- diff --git a/docs/sphinx/user-docs/ray-cluster-interaction.rst b/docs/sphinx/user-docs/ray-cluster-interaction.rst index 8e7929b4..5133ca88 100644 --- a/docs/sphinx/user-docs/ray-cluster-interaction.rst +++ b/docs/sphinx/user-docs/ray-cluster-interaction.rst @@ -30,7 +30,7 @@ of it's usage: ╰─────────────────────────────────────────────────────────────────╯ (, True) cluster.down() - cluster.up() # This function will create an exact copy of the retrieved Ray Cluster only if the Ray Cluster has been previously deleted. + cluster.apply() # This function will create an exact copy of the retrieved Ray Cluster only if the Ray Cluster has been previously deleted. | These are the parameters the ``get_cluster()`` function accepts: | ``cluster_name: str # Required`` -> The name of the Ray Cluster. @@ -61,10 +61,11 @@ list_all_clusters() The following methods require a ``Cluster`` object to be initialized. See :doc:`./cluster-configuration` -cluster.up() +cluster.apply() ------------ -| The ``cluster.up()`` function creates a Ray Cluster in the given namespace. +| The ``cluster.apply()`` function applies a Ray Cluster in the given namespace. If the cluster already exists, it is updated. +| If it does not exist it is created. cluster.down() -------------- diff --git a/docs/sphinx/user-docs/ui-widgets.rst b/docs/sphinx/user-docs/ui-widgets.rst index 92335423..94ddc20a 100644 --- a/docs/sphinx/user-docs/ui-widgets.rst +++ b/docs/sphinx/user-docs/ui-widgets.rst @@ -14,7 +14,7 @@ The Cluster Up/Down buttons appear after successfully initialising your `ClusterConfiguration `__. There are two buttons and a checkbox ``Cluster Up``, ``Cluster Down`` and ``Wait for Cluster?`` which mimic the -`cluster.up() `__, +`cluster.apply() `__, `cluster.down() `__ and `cluster.wait_ready() `__ functionality. diff --git a/poetry.lock b/poetry.lock index 413122c1..0e2d4eac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,155 +1,155 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" -version = "2.4.3" +version = "2.6.1" description = "Happy Eyeballs for asyncio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"}, - {file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"}, + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, ] [[package]] name = "aiohttp" -version = "3.10.11" +version = "3.12.14" description = "Async http client/server framework (asyncio)" optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5077b1a5f40ffa3ba1f40d537d3bec4383988ee51fbba6b74aa8fb1bc466599e"}, - {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d6a14a4d93b5b3c2891fca94fa9d41b2322a68194422bef0dd5ec1e57d7d298"}, - {file = "aiohttp-3.10.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffbfde2443696345e23a3c597049b1dd43049bb65337837574205e7368472177"}, - {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20b3d9e416774d41813bc02fdc0663379c01817b0874b932b81c7f777f67b217"}, - {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b943011b45ee6bf74b22245c6faab736363678e910504dd7531a58c76c9015a"}, - {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48bc1d924490f0d0b3658fe5c4b081a4d56ebb58af80a6729d4bd13ea569797a"}, - {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e12eb3f4b1f72aaaf6acd27d045753b18101524f72ae071ae1c91c1cd44ef115"}, - {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f14ebc419a568c2eff3c1ed35f634435c24ead2fe19c07426af41e7adb68713a"}, - {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:72b191cdf35a518bfc7ca87d770d30941decc5aaf897ec8b484eb5cc8c7706f3"}, - {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5ab2328a61fdc86424ee540d0aeb8b73bbcad7351fb7cf7a6546fc0bcffa0038"}, - {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa93063d4af05c49276cf14e419550a3f45258b6b9d1f16403e777f1addf4519"}, - {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:30283f9d0ce420363c24c5c2421e71a738a2155f10adbb1a11a4d4d6d2715cfc"}, - {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5358addc8044ee49143c546d2182c15b4ac3a60be01c3209374ace05af5733d"}, - {file = "aiohttp-3.10.11-cp310-cp310-win32.whl", hash = "sha256:e1ffa713d3ea7cdcd4aea9cddccab41edf6882fa9552940344c44e59652e1120"}, - {file = "aiohttp-3.10.11-cp310-cp310-win_amd64.whl", hash = "sha256:778cbd01f18ff78b5dd23c77eb82987ee4ba23408cbed233009fd570dda7e674"}, - {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:80ff08556c7f59a7972b1e8919f62e9c069c33566a6d28586771711e0eea4f07"}, - {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c8f96e9ee19f04c4914e4e7a42a60861066d3e1abf05c726f38d9d0a466e695"}, - {file = "aiohttp-3.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fb8601394d537da9221947b5d6e62b064c9a43e88a1ecd7414d21a1a6fba9c24"}, - {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea224cf7bc2d8856d6971cea73b1d50c9c51d36971faf1abc169a0d5f85a382"}, - {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db9503f79e12d5d80b3efd4d01312853565c05367493379df76d2674af881caa"}, - {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f449a50cc33f0384f633894d8d3cd020e3ccef81879c6e6245c3c375c448625"}, - {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82052be3e6d9e0c123499127782a01a2b224b8af8c62ab46b3f6197035ad94e9"}, - {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20063c7acf1eec550c8eb098deb5ed9e1bb0521613b03bb93644b810986027ac"}, - {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:489cced07a4c11488f47aab1f00d0c572506883f877af100a38f1fedaa884c3a"}, - {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea9b3bab329aeaa603ed3bf605f1e2a6f36496ad7e0e1aa42025f368ee2dc07b"}, - {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ca117819d8ad113413016cb29774b3f6d99ad23c220069789fc050267b786c16"}, - {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2dfb612dcbe70fb7cdcf3499e8d483079b89749c857a8f6e80263b021745c730"}, - {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9b615d3da0d60e7d53c62e22b4fd1c70f4ae5993a44687b011ea3a2e49051b8"}, - {file = "aiohttp-3.10.11-cp311-cp311-win32.whl", hash = "sha256:29103f9099b6068bbdf44d6a3d090e0a0b2be6d3c9f16a070dd9d0d910ec08f9"}, - {file = "aiohttp-3.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:236b28ceb79532da85d59aa9b9bf873b364e27a0acb2ceaba475dc61cffb6f3f"}, - {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710"}, - {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d"}, - {file = "aiohttp-3.10.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97"}, - {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725"}, - {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636"}, - {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385"}, - {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087"}, - {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f"}, - {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03"}, - {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d"}, - {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a"}, - {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e"}, - {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4"}, - {file = "aiohttp-3.10.11-cp312-cp312-win32.whl", hash = "sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb"}, - {file = "aiohttp-3.10.11-cp312-cp312-win_amd64.whl", hash = "sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27"}, - {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127"}, - {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413"}, - {file = "aiohttp-3.10.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461"}, - {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288"}, - {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067"}, - {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e"}, - {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1"}, - {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006"}, - {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f"}, - {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6"}, - {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31"}, - {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d"}, - {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00"}, - {file = "aiohttp-3.10.11-cp313-cp313-win32.whl", hash = "sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71"}, - {file = "aiohttp-3.10.11-cp313-cp313-win_amd64.whl", hash = "sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e"}, - {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:74baf1a7d948b3d640badeac333af581a367ab916b37e44cf90a0334157cdfd2"}, - {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:473aebc3b871646e1940c05268d451f2543a1d209f47035b594b9d4e91ce8339"}, - {file = "aiohttp-3.10.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c2f746a6968c54ab2186574e15c3f14f3e7f67aef12b761e043b33b89c5b5f95"}, - {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d110cabad8360ffa0dec8f6ec60e43286e9d251e77db4763a87dcfe55b4adb92"}, - {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0099c7d5d7afff4202a0c670e5b723f7718810000b4abcbc96b064129e64bc7"}, - {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0316e624b754dbbf8c872b62fe6dcb395ef20c70e59890dfa0de9eafccd2849d"}, - {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a5f7ab8baf13314e6b2485965cbacb94afff1e93466ac4d06a47a81c50f9cca"}, - {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c891011e76041e6508cbfc469dd1a8ea09bc24e87e4c204e05f150c4c455a5fa"}, - {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9208299251370ee815473270c52cd3f7069ee9ed348d941d574d1457d2c73e8b"}, - {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:459f0f32c8356e8125f45eeff0ecf2b1cb6db1551304972702f34cd9e6c44658"}, - {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:14cdc8c1810bbd4b4b9f142eeee23cda528ae4e57ea0923551a9af4820980e39"}, - {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:971aa438a29701d4b34e4943e91b5e984c3ae6ccbf80dd9efaffb01bd0b243a9"}, - {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9a309c5de392dfe0f32ee57fa43ed8fc6ddf9985425e84bd51ed66bb16bce3a7"}, - {file = "aiohttp-3.10.11-cp38-cp38-win32.whl", hash = "sha256:9ec1628180241d906a0840b38f162a3215114b14541f1a8711c368a8739a9be4"}, - {file = "aiohttp-3.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:9c6e0ffd52c929f985c7258f83185d17c76d4275ad22e90aa29f38e211aacbec"}, - {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cdc493a2e5d8dc79b2df5bec9558425bcd39aff59fc949810cbd0832e294b106"}, - {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3e70f24e7d0405be2348da9d5a7836936bf3a9b4fd210f8c37e8d48bc32eca6"}, - {file = "aiohttp-3.10.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968b8fb2a5eee2770eda9c7b5581587ef9b96fbdf8dcabc6b446d35ccc69df01"}, - {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deef4362af9493d1382ef86732ee2e4cbc0d7c005947bd54ad1a9a16dd59298e"}, - {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:686b03196976e327412a1b094f4120778c7c4b9cff9bce8d2fdfeca386b89829"}, - {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bf6d027d9d1d34e1c2e1645f18a6498c98d634f8e373395221121f1c258ace8"}, - {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:099fd126bf960f96d34a760e747a629c27fb3634da5d05c7ef4d35ef4ea519fc"}, - {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c73c4d3dae0b4644bc21e3de546530531d6cdc88659cdeb6579cd627d3c206aa"}, - {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c5580f3c51eea91559db3facd45d72e7ec970b04528b4709b1f9c2555bd6d0b"}, - {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fdf6429f0caabfd8a30c4e2eaecb547b3c340e4730ebfe25139779b9815ba138"}, - {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d97187de3c276263db3564bb9d9fad9e15b51ea10a371ffa5947a5ba93ad6777"}, - {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0acafb350cfb2eba70eb5d271f55e08bd4502ec35e964e18ad3e7d34d71f7261"}, - {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c13ed0c779911c7998a58e7848954bd4d63df3e3575f591e321b19a2aec8df9f"}, - {file = "aiohttp-3.10.11-cp39-cp39-win32.whl", hash = "sha256:22b7c540c55909140f63ab4f54ec2c20d2635c0289cdd8006da46f3327f971b9"}, - {file = "aiohttp-3.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:7b26b1551e481012575dab8e3727b16fe7dd27eb2711d2e63ced7368756268fb"}, - {file = "aiohttp-3.10.11.tar.gz", hash = "sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7"}, +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:906d5075b5ba0dd1c66fcaaf60eb09926a9fef3ca92d912d2a0bbdbecf8b1248"}, + {file = "aiohttp-3.12.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c875bf6fc2fd1a572aba0e02ef4e7a63694778c5646cdbda346ee24e630d30fb"}, + {file = "aiohttp-3.12.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbb284d15c6a45fab030740049d03c0ecd60edad9cd23b211d7e11d3be8d56fd"}, + {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e360381e02e1a05d36b223ecab7bc4a6e7b5ab15760022dc92589ee1d4238c"}, + {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aaf90137b5e5d84a53632ad95ebee5c9e3e7468f0aab92ba3f608adcb914fa95"}, + {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e532a25e4a0a2685fa295a31acf65e027fbe2bea7a4b02cdfbbba8a064577663"}, + {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eab9762c4d1b08ae04a6c77474e6136da722e34fdc0e6d6eab5ee93ac29f35d1"}, + {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abe53c3812b2899889a7fca763cdfaeee725f5be68ea89905e4275476ffd7e61"}, + {file = "aiohttp-3.12.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5760909b7080aa2ec1d320baee90d03b21745573780a072b66ce633eb77a8656"}, + {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:02fcd3f69051467bbaa7f84d7ec3267478c7df18d68b2e28279116e29d18d4f3"}, + {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4dcd1172cd6794884c33e504d3da3c35648b8be9bfa946942d353b939d5f1288"}, + {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:224d0da41355b942b43ad08101b1b41ce633a654128ee07e36d75133443adcda"}, + {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e387668724f4d734e865c1776d841ed75b300ee61059aca0b05bce67061dcacc"}, + {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:dec9cde5b5a24171e0b0a4ca064b1414950904053fb77c707efd876a2da525d8"}, + {file = "aiohttp-3.12.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bbad68a2af4877cc103cd94af9160e45676fc6f0c14abb88e6e092b945c2c8e3"}, + {file = "aiohttp-3.12.14-cp310-cp310-win32.whl", hash = "sha256:ee580cb7c00bd857b3039ebca03c4448e84700dc1322f860cf7a500a6f62630c"}, + {file = "aiohttp-3.12.14-cp310-cp310-win_amd64.whl", hash = "sha256:cf4f05b8cea571e2ccc3ca744e35ead24992d90a72ca2cf7ab7a2efbac6716db"}, + {file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597"}, + {file = "aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393"}, + {file = "aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179"}, + {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb"}, + {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245"}, + {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b"}, + {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641"}, + {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe"}, + {file = "aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7"}, + {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635"}, + {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da"}, + {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419"}, + {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab"}, + {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0"}, + {file = "aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28"}, + {file = "aiohttp-3.12.14-cp311-cp311-win32.whl", hash = "sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b"}, + {file = "aiohttp-3.12.14-cp311-cp311-win_amd64.whl", hash = "sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced"}, + {file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22"}, + {file = "aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a"}, + {file = "aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff"}, + {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d"}, + {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869"}, + {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c"}, + {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7"}, + {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660"}, + {file = "aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088"}, + {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7"}, + {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9"}, + {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3"}, + {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb"}, + {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425"}, + {file = "aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0"}, + {file = "aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729"}, + {file = "aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338"}, + {file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767"}, + {file = "aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e"}, + {file = "aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63"}, + {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d"}, + {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab"}, + {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4"}, + {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026"}, + {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd"}, + {file = "aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88"}, + {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086"}, + {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933"}, + {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151"}, + {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8"}, + {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3"}, + {file = "aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758"}, + {file = "aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5"}, + {file = "aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa"}, + {file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b8cc6b05e94d837bcd71c6531e2344e1ff0fb87abe4ad78a9261d67ef5d83eae"}, + {file = "aiohttp-3.12.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1dcb015ac6a3b8facd3677597edd5ff39d11d937456702f0bb2b762e390a21b"}, + {file = "aiohttp-3.12.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3779ed96105cd70ee5e85ca4f457adbce3d9ff33ec3d0ebcdf6c5727f26b21b3"}, + {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:717a0680729b4ebd7569c1dcd718c46b09b360745fd8eb12317abc74b14d14d0"}, + {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b5dd3a2ef7c7e968dbbac8f5574ebeac4d2b813b247e8cec28174a2ba3627170"}, + {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4710f77598c0092239bc12c1fcc278a444e16c7032d91babf5abbf7166463f7b"}, + {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f3e9f75ae842a6c22a195d4a127263dbf87cbab729829e0bd7857fb1672400b2"}, + {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9c8d55d6802086edd188e3a7d85a77787e50d56ce3eb4757a3205fa4657922"}, + {file = "aiohttp-3.12.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79b29053ff3ad307880d94562cca80693c62062a098a5776ea8ef5ef4b28d140"}, + {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23e1332fff36bebd3183db0c7a547a1da9d3b4091509f6d818e098855f2f27d3"}, + {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a564188ce831fd110ea76bcc97085dd6c625b427db3f1dbb14ca4baa1447dcbc"}, + {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a7a1b4302f70bb3ec40ca86de82def532c97a80db49cac6a6700af0de41af5ee"}, + {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1b07ccef62950a2519f9bfc1e5b294de5dd84329f444ca0b329605ea787a3de5"}, + {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:938bd3ca6259e7e48b38d84f753d548bd863e0c222ed6ee6ace3fd6752768a84"}, + {file = "aiohttp-3.12.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8bc784302b6b9f163b54c4e93d7a6f09563bd01ff2b841b29ed3ac126e5040bf"}, + {file = "aiohttp-3.12.14-cp39-cp39-win32.whl", hash = "sha256:a3416f95961dd7d5393ecff99e3f41dc990fb72eda86c11f2a60308ac6dcd7a0"}, + {file = "aiohttp-3.12.14-cp39-cp39-win_amd64.whl", hash = "sha256:196858b8820d7f60578f8b47e5669b3195c21d8ab261e39b1d705346458f445f"}, + {file = "aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2"}, ] [package.dependencies] -aiohappyeyeballs = ">=2.3.0" -aiosignal = ">=1.1.2" -async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" -yarl = ">=1.12.0,<2.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aiohttp-cors" -version = "0.7.0" +version = "0.8.1" description = "CORS support for aiohttp" optional = false -python-versions = "*" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d"}, - {file = "aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e"}, + {file = "aiohttp_cors-0.8.1-py3-none-any.whl", hash = "sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d"}, + {file = "aiohttp_cors-0.8.1.tar.gz", hash = "sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403"}, ] [package.dependencies] -aiohttp = ">=1.1" +aiohttp = ">=3.9" [[package]] name = "aiosignal" -version = "1.3.1" +version = "1.4.0" description = "aiosignal: a list of registered asynchronous callbacks" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, ] [package.dependencies] frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "alabaster" @@ -157,31 +157,44 @@ version = "0.7.16" description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" -version = "4.6.0" +version = "4.9.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ - {file = "anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a"}, - {file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"}, + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, ] [package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -190,6 +203,8 @@ version = "0.1.4" description = "Disable App Nap on macOS >= 10.9" optional = false python-versions = ">=3.6" +groups = ["test"] +markers = "platform_system == \"Darwin\"" files = [ {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, @@ -197,30 +212,26 @@ files = [ [[package]] name = "argon2-cffi" -version = "23.1.0" +version = "25.1.0" description = "Argon2 for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, - {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, + {file = "argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741"}, + {file = "argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1"}, ] [package.dependencies] argon2-cffi-bindings = "*" -[package.extras] -dev = ["argon2-cffi[tests,typing]", "tox (>4)"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] -tests = ["hypothesis", "pytest"] -typing = ["mypy"] - [[package]] name = "argon2-cffi-bindings" version = "21.2.0" description = "Low-level CFFI bindings for Argon2" optional = false python-versions = ">=3.6" +groups = ["test"] files = [ {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, @@ -258,6 +269,7 @@ version = "1.3.0" description = "Better dates & times for Python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, @@ -273,114 +285,126 @@ test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock [[package]] name = "asttokens" -version = "2.4.1" +version = "3.0.0" description = "Annotate AST trees with source code positions" optional = false -python-versions = "*" +python-versions = ">=3.8" +groups = ["main", "test"] files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, + {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, + {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, ] -[package.dependencies] -six = ">=1.12.0" - [package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] +astroid = ["astroid (>=2,<4)"] +test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "async-lru" -version = "2.0.4" +version = "2.0.5" description = "Simple LRU cache for asyncio" optional = false -python-versions = ">=3.8" -files = [ - {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, - {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["test"] files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, + {file = "async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943"}, + {file = "async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb"}, ] [[package]] name = "attrs" -version = "24.2.0" +version = "25.3.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["main", "test"] files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "babel" -version = "2.16.0" +version = "2.17.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" +groups = ["docs", "test"] files = [ - {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, - {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] [package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "bcrypt" -version = "4.2.0" +version = "4.3.0" description = "Modern password hashing for your software and your servers" optional = false -python-versions = ">=3.7" -files = [ - {file = "bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7"}, - {file = "bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458"}, - {file = "bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5"}, - {file = "bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8"}, - {file = "bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34"}, - {file = "bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9"}, - {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a"}, - {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db"}, - {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170"}, - {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184"}, - {file = "bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221"}, +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"}, + {file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"}, + {file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"}, + {file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"}, + {file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"}, + {file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"}, + {file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"}, + {file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"}, ] [package.extras] @@ -389,17 +413,19 @@ typecheck = ["mypy"] [[package]] name = "beautifulsoup4" -version = "4.12.3" +version = "4.13.4" description = "Screen-scraping library" optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.7.0" +groups = ["test"] files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, + {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, + {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, ] [package.dependencies] soupsieve = ">1.2" +typing-extensions = ">=4.0.0" [package.extras] cchardet = ["cchardet"] @@ -410,42 +436,45 @@ lxml = ["lxml"] [[package]] name = "bleach" -version = "6.1.0" +version = "6.2.0" description = "An easy safelist-based HTML-sanitizing tool." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["test"] files = [ - {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, - {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, + {file = "bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e"}, + {file = "bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f"}, ] [package.dependencies] -six = ">=1.9.0" +tinycss2 = {version = ">=1.1.0,<1.5", optional = true, markers = "extra == \"css\""} webencodings = "*" [package.extras] -css = ["tinycss2 (>=1.1.0,<1.3)"] +css = ["tinycss2 (>=1.1.0,<1.5)"] [[package]] name = "cachetools" -version = "5.5.0" +version = "5.5.2" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, - {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, + {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, + {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, ] [[package]] name = "certifi" -version = "2024.8.30" +version = "2025.7.9" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +groups = ["main", "docs", "test"] files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39"}, + {file = "certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079"}, ] [[package]] @@ -454,6 +483,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -527,114 +557,130 @@ files = [ [package.dependencies] pycparser = "*" +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.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"}, +python-versions = ">=3.7" +groups = ["main", "docs", "test"] +files = [ + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.2.1" description = "Composable command line interface toolkit" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" +groups = ["main"] files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, ] [package.dependencies] @@ -646,20 +692,23 @@ 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" +groups = ["main", "docs", "test"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", docs = "sys_platform == \"win32\"", test = "sys_platform == \"win32\""} [[package]] name = "colorful" -version = "0.5.6" +version = "0.5.7" description = "Terminal string styling done right, in Python." optional = false python-versions = "*" +groups = ["main"] files = [ - {file = "colorful-0.5.6-py2.py3-none-any.whl", hash = "sha256:eab8c1c809f5025ad2b5238a50bd691e26850da8cac8f90d660ede6ea1af9f1e"}, - {file = "colorful-0.5.6.tar.gz", hash = "sha256:b56d5c01db1dac4898308ea889edcb113fbee3e6ec5df4bacffd61d5241b5b8d"}, + {file = "colorful-0.5.7-py2.py3-none-any.whl", hash = "sha256:495dd3a23151a9568cee8a90fc1174c902ad7ef06655f50b6bddf9e80008da69"}, + {file = "colorful-0.5.7.tar.gz", hash = "sha256:c5452179b56601c178b03d468a5326cc1fe37d9be81d24d0d6bdab36c4b93ad8"}, ] [package.dependencies] @@ -671,6 +720,7 @@ version = "0.2.2" description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, @@ -688,6 +738,7 @@ version = "7.6.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, @@ -754,7 +805,7 @@ files = [ ] [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" @@ -762,6 +813,7 @@ version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, @@ -807,44 +859,50 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "debugpy" -version = "1.8.6" +version = "1.8.14" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" -files = [ - {file = "debugpy-1.8.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:30f467c5345d9dfdcc0afdb10e018e47f092e383447500f125b4e013236bf14b"}, - {file = "debugpy-1.8.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d73d8c52614432f4215d0fe79a7e595d0dd162b5c15233762565be2f014803b"}, - {file = "debugpy-1.8.6-cp310-cp310-win32.whl", hash = "sha256:e3e182cd98eac20ee23a00653503315085b29ab44ed66269482349d307b08df9"}, - {file = "debugpy-1.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:e3a82da039cfe717b6fb1886cbbe5c4a3f15d7df4765af857f4307585121c2dd"}, - {file = "debugpy-1.8.6-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:67479a94cf5fd2c2d88f9615e087fcb4fec169ec780464a3f2ba4a9a2bb79955"}, - {file = "debugpy-1.8.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb8653f6cbf1dd0a305ac1aa66ec246002145074ea57933978346ea5afdf70b"}, - {file = "debugpy-1.8.6-cp311-cp311-win32.whl", hash = "sha256:cdaf0b9691879da2d13fa39b61c01887c34558d1ff6e5c30e2eb698f5384cd43"}, - {file = "debugpy-1.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:43996632bee7435583952155c06881074b9a742a86cee74e701d87ca532fe833"}, - {file = "debugpy-1.8.6-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:db891b141fc6ee4b5fc6d1cc8035ec329cabc64bdd2ae672b4550c87d4ecb128"}, - {file = "debugpy-1.8.6-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:567419081ff67da766c898ccf21e79f1adad0e321381b0dfc7a9c8f7a9347972"}, - {file = "debugpy-1.8.6-cp312-cp312-win32.whl", hash = "sha256:c9834dfd701a1f6bf0f7f0b8b1573970ae99ebbeee68314116e0ccc5c78eea3c"}, - {file = "debugpy-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:e4ce0570aa4aca87137890d23b86faeadf184924ad892d20c54237bcaab75d8f"}, - {file = "debugpy-1.8.6-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:df5dc9eb4ca050273b8e374a4cd967c43be1327eeb42bfe2f58b3cdfe7c68dcb"}, - {file = "debugpy-1.8.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a85707c6a84b0c5b3db92a2df685b5230dd8fb8c108298ba4f11dba157a615a"}, - {file = "debugpy-1.8.6-cp38-cp38-win32.whl", hash = "sha256:538c6cdcdcdad310bbefd96d7850be1cd46e703079cc9e67d42a9ca776cdc8a8"}, - {file = "debugpy-1.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:22140bc02c66cda6053b6eb56dfe01bbe22a4447846581ba1dd6df2c9f97982d"}, - {file = "debugpy-1.8.6-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:c1cef65cffbc96e7b392d9178dbfd524ab0750da6c0023c027ddcac968fd1caa"}, - {file = "debugpy-1.8.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e60bd06bb3cc5c0e957df748d1fab501e01416c43a7bdc756d2a992ea1b881"}, - {file = "debugpy-1.8.6-cp39-cp39-win32.whl", hash = "sha256:f7158252803d0752ed5398d291dee4c553bb12d14547c0e1843ab74ee9c31123"}, - {file = "debugpy-1.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:3358aa619a073b620cd0d51d8a6176590af24abcc3fe2e479929a154bf591b51"}, - {file = "debugpy-1.8.6-py2.py3-none-any.whl", hash = "sha256:b48892df4d810eff21d3ef37274f4c60d32cdcafc462ad5647239036b0f0649f"}, - {file = "debugpy-1.8.6.zip", hash = "sha256:c931a9371a86784cee25dec8d65bc2dc7a21f3f1552e3833d9ef8f919d22280a"}, +groups = ["test"] +files = [ + {file = "debugpy-1.8.14-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:93fee753097e85623cab1c0e6a68c76308cd9f13ffdf44127e6fab4fbf024339"}, + {file = "debugpy-1.8.14-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d937d93ae4fa51cdc94d3e865f535f185d5f9748efb41d0d49e33bf3365bd79"}, + {file = "debugpy-1.8.14-cp310-cp310-win32.whl", hash = "sha256:c442f20577b38cc7a9aafecffe1094f78f07fb8423c3dddb384e6b8f49fd2987"}, + {file = "debugpy-1.8.14-cp310-cp310-win_amd64.whl", hash = "sha256:f117dedda6d969c5c9483e23f573b38f4e39412845c7bc487b6f2648df30fe84"}, + {file = "debugpy-1.8.14-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:1b2ac8c13b2645e0b1eaf30e816404990fbdb168e193322be8f545e8c01644a9"}, + {file = "debugpy-1.8.14-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf431c343a99384ac7eab2f763980724834f933a271e90496944195318c619e2"}, + {file = "debugpy-1.8.14-cp311-cp311-win32.whl", hash = "sha256:c99295c76161ad8d507b413cd33422d7c542889fbb73035889420ac1fad354f2"}, + {file = "debugpy-1.8.14-cp311-cp311-win_amd64.whl", hash = "sha256:7816acea4a46d7e4e50ad8d09d963a680ecc814ae31cdef3622eb05ccacf7b01"}, + {file = "debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84"}, + {file = "debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826"}, + {file = "debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f"}, + {file = "debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f"}, + {file = "debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f"}, + {file = "debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15"}, + {file = "debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e"}, + {file = "debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e"}, + {file = "debugpy-1.8.14-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:d5582bcbe42917bc6bbe5c12db1bffdf21f6bfc28d4554b738bf08d50dc0c8c3"}, + {file = "debugpy-1.8.14-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5349b7c3735b766a281873fbe32ca9cca343d4cc11ba4a743f84cb854339ff35"}, + {file = "debugpy-1.8.14-cp38-cp38-win32.whl", hash = "sha256:7118d462fe9724c887d355eef395fae68bc764fd862cdca94e70dcb9ade8a23d"}, + {file = "debugpy-1.8.14-cp38-cp38-win_amd64.whl", hash = "sha256:d235e4fa78af2de4e5609073972700523e372cf5601742449970110d565ca28c"}, + {file = "debugpy-1.8.14-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:413512d35ff52c2fb0fd2d65e69f373ffd24f0ecb1fac514c04a668599c5ce7f"}, + {file = "debugpy-1.8.14-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c9156f7524a0d70b7a7e22b2e311d8ba76a15496fb00730e46dcdeedb9e1eea"}, + {file = "debugpy-1.8.14-cp39-cp39-win32.whl", hash = "sha256:b44985f97cc3dd9d52c42eb59ee9d7ee0c4e7ecd62bca704891f997de4cef23d"}, + {file = "debugpy-1.8.14-cp39-cp39-win_amd64.whl", hash = "sha256:b1528cfee6c1b1c698eb10b6b096c598738a8238822d218173d21c3086de8123"}, + {file = "debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20"}, + {file = "debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322"}, ] [[package]] name = "decorator" -version = "5.1.1" +version = "5.2.1" description = "Decorators for Humans" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" +groups = ["main", "test"] files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, + {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, + {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, ] [[package]] @@ -853,70 +911,94 @@ version = "0.7.1" description = "XML bomb protection for Python stdlib modules" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["test"] files = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] +[[package]] +name = "diff-cover" +version = "9.6.0" +description = "Run coverage and linting reports on diffs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "diff_cover-9.6.0-py3-none-any.whl", hash = "sha256:29fbeb52d77a0b8c811e5580d5dbf41801a838da2ed54319a599da8f7233c547"}, + {file = "diff_cover-9.6.0.tar.gz", hash = "sha256:75e5bc056dcaa68c6c87c9fb4e07c9e60daef15b6e8d034d56d2da9e2c84a872"}, +] + +[package.dependencies] +chardet = ">=3.0.0" +Jinja2 = ">=2.7.1" +pluggy = ">=0.13.1,<2" +Pygments = ">=2.19.1,<3.0.0" + +[package.extras] +toml = ["tomli (>=1.2.1)"] + [[package]] name = "distlib" -version = "0.3.8" +version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["main"] files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] [[package]] name = "docutils" -version = "0.20.1" +version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["docs"] files = [ - {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, - {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" +name = "durationpy" +version = "0.10" +description = "Module for converting between datetime.timedelta and Go's Duration strings." optional = false -python-versions = ">=3.7" +python-versions = "*" +groups = ["main"] files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286"}, + {file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"}, ] -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "executing" version = "1.2.0" description = "Get the currently executing AST node of a frame, and other information" optional = false python-versions = "*" +groups = ["main", "test"] files = [ {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, ] [package.extras] -tests = ["asttokens", "littleutils", "pytest", "rich"] +tests = ["asttokens", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] [[package]] name = "fastjsonschema" -version = "2.20.0" +version = "2.21.1" description = "Fastest Python implementation of JSON schema" optional = false python-versions = "*" +groups = ["test"] files = [ - {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, - {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, + {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, + {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, ] [package.extras] @@ -924,19 +1006,20 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.16.1" +version = "3.18.0" description = "A platform independent file lock." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, - {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "fqdn" @@ -944,6 +1027,7 @@ version = "1.5.1" description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" optional = false python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" +groups = ["test"] files = [ {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, @@ -951,99 +1035,128 @@ files = [ [[package]] name = "frozenlist" -version = "1.4.1" +version = "1.7.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false -python-versions = ">=3.8" -files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718"}, + {file = "frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e"}, + {file = "frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56"}, + {file = "frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7"}, + {file = "frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43"}, + {file = "frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3"}, + {file = "frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e"}, + {file = "frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1"}, + {file = "frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-win32.whl", hash = "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e"}, + {file = "frozenlist-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63"}, + {file = "frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e"}, + {file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"}, ] [[package]] name = "fsspec" -version = "2024.9.0" +version = "2025.5.1" description = "File-system specification" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "fsspec-2024.9.0-py3-none-any.whl", hash = "sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b"}, - {file = "fsspec-2024.9.0.tar.gz", hash = "sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8"}, + {file = "fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462"}, + {file = "fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475"}, ] [package.extras] @@ -1070,42 +1183,48 @@ sftp = ["paramiko"] smb = ["smbprotocol"] ssh = ["paramiko"] test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] -test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] +test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] tqdm = ["tqdm"] [[package]] name = "google-api-core" -version = "2.20.0" +version = "2.25.1" description = "Google API client core library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "google_api_core-2.20.0-py3-none-any.whl", hash = "sha256:ef0591ef03c30bb83f79b3d0575c3f31219001fc9c5cf37024d08310aeffed8a"}, - {file = "google_api_core-2.20.0.tar.gz", hash = "sha256:f74dff1889ba291a4b76c5079df0711810e2d9da81abfdc99957bc961c1eb28f"}, + {file = "google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7"}, + {file = "google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8"}, ] [package.dependencies] -google-auth = ">=2.14.1,<3.0.dev0" -googleapis-common-protos = ">=1.56.2,<2.0.dev0" -proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" -requests = ">=2.18.0,<3.0.0.dev0" +google-auth = ">=2.14.1,<3.0.0" +googleapis-common-protos = ">=1.56.2,<2.0.0" +proto-plus = [ + {version = ">=1.22.3,<2.0.0"}, + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, +] +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" +requests = ">=2.18.0,<3.0.0" [package.extras] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] -grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] -grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.0)"] +grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0) ; python_version >= \"3.11\""] +grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] [[package]] name = "google-auth" -version = "2.35.0" +version = "2.40.3" description = "Google Authentication Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "google_auth-2.35.0-py2.py3-none-any.whl", hash = "sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f"}, - {file = "google_auth-2.35.0.tar.gz", hash = "sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a"}, + {file = "google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca"}, + {file = "google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77"}, ] [package.dependencies] @@ -1114,137 +1233,141 @@ pyasn1-modules = ">=0.2.1" rsa = ">=3.1.4,<5" [package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] enterprise-cert = ["cryptography", "pyopenssl"] -pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +pyjwt = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0.dev0)"] +requests = ["requests (>=2.20.0,<3.0.0)"] +testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +urllib3 = ["packaging", "urllib3"] [[package]] name = "googleapis-common-protos" -version = "1.65.0" +version = "1.70.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "googleapis_common_protos-1.65.0-py2.py3-none-any.whl", hash = "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63"}, - {file = "googleapis_common_protos-1.65.0.tar.gz", hash = "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0"}, + {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, + {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, ] [package.dependencies] -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" [package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] [[package]] name = "grpcio" -version = "1.66.2" +version = "1.73.1" description = "HTTP/2-based RPC framework" optional = false -python-versions = ">=3.8" -files = [ - {file = "grpcio-1.66.2-cp310-cp310-linux_armv7l.whl", hash = "sha256:fe96281713168a3270878255983d2cb1a97e034325c8c2c25169a69289d3ecfa"}, - {file = "grpcio-1.66.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:73fc8f8b9b5c4a03e802b3cd0c18b2b06b410d3c1dcbef989fdeb943bd44aff7"}, - {file = "grpcio-1.66.2-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:03b0b307ba26fae695e067b94cbb014e27390f8bc5ac7a3a39b7723fed085604"}, - {file = "grpcio-1.66.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d69ce1f324dc2d71e40c9261d3fdbe7d4c9d60f332069ff9b2a4d8a257c7b2b"}, - {file = "grpcio-1.66.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05bc2ceadc2529ab0b227b1310d249d95d9001cd106aa4d31e8871ad3c428d73"}, - {file = "grpcio-1.66.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ac475e8da31484efa25abb774674d837b343afb78bb3bcdef10f81a93e3d6bf"}, - {file = "grpcio-1.66.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0be4e0490c28da5377283861bed2941d1d20ec017ca397a5df4394d1c31a9b50"}, - {file = "grpcio-1.66.2-cp310-cp310-win32.whl", hash = "sha256:4e504572433f4e72b12394977679161d495c4c9581ba34a88d843eaf0f2fbd39"}, - {file = "grpcio-1.66.2-cp310-cp310-win_amd64.whl", hash = "sha256:2018b053aa15782db2541ca01a7edb56a0bf18c77efed975392583725974b249"}, - {file = "grpcio-1.66.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:2335c58560a9e92ac58ff2bc5649952f9b37d0735608242973c7a8b94a6437d8"}, - {file = "grpcio-1.66.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45a3d462826f4868b442a6b8fdbe8b87b45eb4f5b5308168c156b21eca43f61c"}, - {file = "grpcio-1.66.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a9539f01cb04950fd4b5ab458e64a15f84c2acc273670072abe49a3f29bbad54"}, - {file = "grpcio-1.66.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce89f5876662f146d4c1f695dda29d4433a5d01c8681fbd2539afff535da14d4"}, - {file = "grpcio-1.66.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25a14af966438cddf498b2e338f88d1c9706f3493b1d73b93f695c99c5f0e2a"}, - {file = "grpcio-1.66.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6001e575b8bbd89eee11960bb640b6da6ae110cf08113a075f1e2051cc596cae"}, - {file = "grpcio-1.66.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4ea1d062c9230278793820146c95d038dc0f468cbdd172eec3363e42ff1c7d01"}, - {file = "grpcio-1.66.2-cp311-cp311-win32.whl", hash = "sha256:38b68498ff579a3b1ee8f93a05eb48dc2595795f2f62716e797dc24774c1aaa8"}, - {file = "grpcio-1.66.2-cp311-cp311-win_amd64.whl", hash = "sha256:6851de821249340bdb100df5eacfecfc4e6075fa85c6df7ee0eb213170ec8e5d"}, - {file = "grpcio-1.66.2-cp312-cp312-linux_armv7l.whl", hash = "sha256:802d84fd3d50614170649853d121baaaa305de7b65b3e01759247e768d691ddf"}, - {file = "grpcio-1.66.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:80fd702ba7e432994df208f27514280b4b5c6843e12a48759c9255679ad38db8"}, - {file = "grpcio-1.66.2-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:12fda97ffae55e6526825daf25ad0fa37483685952b5d0f910d6405c87e3adb6"}, - {file = "grpcio-1.66.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:950da58d7d80abd0ea68757769c9db0a95b31163e53e5bb60438d263f4bed7b7"}, - {file = "grpcio-1.66.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e636ce23273683b00410f1971d209bf3689238cf5538d960adc3cdfe80dd0dbd"}, - {file = "grpcio-1.66.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a917d26e0fe980b0ac7bfcc1a3c4ad6a9a4612c911d33efb55ed7833c749b0ee"}, - {file = "grpcio-1.66.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49f0ca7ae850f59f828a723a9064cadbed90f1ece179d375966546499b8a2c9c"}, - {file = "grpcio-1.66.2-cp312-cp312-win32.whl", hash = "sha256:31fd163105464797a72d901a06472860845ac157389e10f12631025b3e4d0453"}, - {file = "grpcio-1.66.2-cp312-cp312-win_amd64.whl", hash = "sha256:ff1f7882e56c40b0d33c4922c15dfa30612f05fb785074a012f7cda74d1c3679"}, - {file = "grpcio-1.66.2-cp313-cp313-linux_armv7l.whl", hash = "sha256:3b00efc473b20d8bf83e0e1ae661b98951ca56111feb9b9611df8efc4fe5d55d"}, - {file = "grpcio-1.66.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1caa38fb22a8578ab8393da99d4b8641e3a80abc8fd52646f1ecc92bcb8dee34"}, - {file = "grpcio-1.66.2-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:c408f5ef75cfffa113cacd8b0c0e3611cbfd47701ca3cdc090594109b9fcbaed"}, - {file = "grpcio-1.66.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c806852deaedee9ce8280fe98955c9103f62912a5b2d5ee7e3eaa284a6d8d8e7"}, - {file = "grpcio-1.66.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f145cc21836c332c67baa6fc81099d1d27e266401565bf481948010d6ea32d46"}, - {file = "grpcio-1.66.2-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:73e3b425c1e155730273f73e419de3074aa5c5e936771ee0e4af0814631fb30a"}, - {file = "grpcio-1.66.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c509a4f78114cbc5f0740eb3d7a74985fd2eff022971bc9bc31f8bc93e66a3b"}, - {file = "grpcio-1.66.2-cp313-cp313-win32.whl", hash = "sha256:20657d6b8cfed7db5e11b62ff7dfe2e12064ea78e93f1434d61888834bc86d75"}, - {file = "grpcio-1.66.2-cp313-cp313-win_amd64.whl", hash = "sha256:fb70487c95786e345af5e854ffec8cb8cc781bcc5df7930c4fbb7feaa72e1cdf"}, - {file = "grpcio-1.66.2-cp38-cp38-linux_armv7l.whl", hash = "sha256:a18e20d8321c6400185b4263e27982488cb5cdd62da69147087a76a24ef4e7e3"}, - {file = "grpcio-1.66.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:02697eb4a5cbe5a9639f57323b4c37bcb3ab2d48cec5da3dc2f13334d72790dd"}, - {file = "grpcio-1.66.2-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:99a641995a6bc4287a6315989ee591ff58507aa1cbe4c2e70d88411c4dcc0839"}, - {file = "grpcio-1.66.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ed71e81782966ffead60268bbda31ea3f725ebf8aa73634d5dda44f2cf3fb9c"}, - {file = "grpcio-1.66.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbd27c24a4cc5e195a7f56cfd9312e366d5d61b86e36d46bbe538457ea6eb8dd"}, - {file = "grpcio-1.66.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d9a9724a156c8ec6a379869b23ba3323b7ea3600851c91489b871e375f710bc8"}, - {file = "grpcio-1.66.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d8d4732cc5052e92cea2f78b233c2e2a52998ac40cd651f40e398893ad0d06ec"}, - {file = "grpcio-1.66.2-cp38-cp38-win32.whl", hash = "sha256:7b2c86457145ce14c38e5bf6bdc19ef88e66c5fee2c3d83285c5aef026ba93b3"}, - {file = "grpcio-1.66.2-cp38-cp38-win_amd64.whl", hash = "sha256:e88264caad6d8d00e7913996030bac8ad5f26b7411495848cc218bd3a9040b6c"}, - {file = "grpcio-1.66.2-cp39-cp39-linux_armv7l.whl", hash = "sha256:c400ba5675b67025c8a9f48aa846f12a39cf0c44df5cd060e23fda5b30e9359d"}, - {file = "grpcio-1.66.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:66a0cd8ba6512b401d7ed46bb03f4ee455839957f28b8d61e7708056a806ba6a"}, - {file = "grpcio-1.66.2-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:06de8ec0bd71be123eec15b0e0d457474931c2c407869b6c349bd9bed4adbac3"}, - {file = "grpcio-1.66.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb57870449dfcfac428afbb5a877829fcb0d6db9d9baa1148705739e9083880e"}, - {file = "grpcio-1.66.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b672abf90a964bfde2d0ecbce30f2329a47498ba75ce6f4da35a2f4532b7acbc"}, - {file = "grpcio-1.66.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ad2efdbe90c73b0434cbe64ed372e12414ad03c06262279b104a029d1889d13e"}, - {file = "grpcio-1.66.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9c3a99c519f4638e700e9e3f83952e27e2ea10873eecd7935823dab0c1c9250e"}, - {file = "grpcio-1.66.2-cp39-cp39-win32.whl", hash = "sha256:78fa51ebc2d9242c0fc5db0feecc57a9943303b46664ad89921f5079e2e4ada7"}, - {file = "grpcio-1.66.2-cp39-cp39-win_amd64.whl", hash = "sha256:728bdf36a186e7f51da73be7f8d09457a03061be848718d0edf000e709418987"}, - {file = "grpcio-1.66.2.tar.gz", hash = "sha256:563588c587b75c34b928bc428548e5b00ea38c46972181a4d8b75ba7e3f24231"}, +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "grpcio-1.73.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:2d70f4ddd0a823436c2624640570ed6097e40935c9194482475fe8e3d9754d55"}, + {file = "grpcio-1.73.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:3841a8a5a66830261ab6a3c2a3dc539ed84e4ab019165f77b3eeb9f0ba621f26"}, + {file = "grpcio-1.73.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:628c30f8e77e0258ab788750ec92059fc3d6628590fb4b7cea8c102503623ed7"}, + {file = "grpcio-1.73.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67a0468256c9db6d5ecb1fde4bf409d016f42cef649323f0a08a72f352d1358b"}, + {file = "grpcio-1.73.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b84d65bbdebd5926eb5c53b0b9ec3b3f83408a30e4c20c373c5337b4219ec5"}, + {file = "grpcio-1.73.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c54796ca22b8349cc594d18b01099e39f2b7ffb586ad83217655781a350ce4da"}, + {file = "grpcio-1.73.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:75fc8e543962ece2f7ecd32ada2d44c0c8570ae73ec92869f9af8b944863116d"}, + {file = "grpcio-1.73.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6a6037891cd2b1dd1406b388660522e1565ed340b1fea2955b0234bdd941a862"}, + {file = "grpcio-1.73.1-cp310-cp310-win32.whl", hash = "sha256:cce7265b9617168c2d08ae570fcc2af4eaf72e84f8c710ca657cc546115263af"}, + {file = "grpcio-1.73.1-cp310-cp310-win_amd64.whl", hash = "sha256:6a2b372e65fad38842050943f42ce8fee00c6f2e8ea4f7754ba7478d26a356ee"}, + {file = "grpcio-1.73.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:ba2cea9f7ae4bc21f42015f0ec98f69ae4179848ad744b210e7685112fa507a1"}, + {file = "grpcio-1.73.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d74c3f4f37b79e746271aa6cdb3a1d7e4432aea38735542b23adcabaaee0c097"}, + {file = "grpcio-1.73.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:5b9b1805a7d61c9e90541cbe8dfe0a593dfc8c5c3a43fe623701b6a01b01d710"}, + {file = "grpcio-1.73.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3215f69a0670a8cfa2ab53236d9e8026bfb7ead5d4baabe7d7dc11d30fda967"}, + {file = "grpcio-1.73.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc5eccfd9577a5dc7d5612b2ba90cca4ad14c6d949216c68585fdec9848befb1"}, + {file = "grpcio-1.73.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dc7d7fd520614fce2e6455ba89791458020a39716951c7c07694f9dbae28e9c0"}, + {file = "grpcio-1.73.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:105492124828911f85127e4825d1c1234b032cb9d238567876b5515d01151379"}, + {file = "grpcio-1.73.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:610e19b04f452ba6f402ac9aa94eb3d21fbc94553368008af634812c4a85a99e"}, + {file = "grpcio-1.73.1-cp311-cp311-win32.whl", hash = "sha256:d60588ab6ba0ac753761ee0e5b30a29398306401bfbceffe7d68ebb21193f9d4"}, + {file = "grpcio-1.73.1-cp311-cp311-win_amd64.whl", hash = "sha256:6957025a4608bb0a5ff42abd75bfbb2ed99eda29d5992ef31d691ab54b753643"}, + {file = "grpcio-1.73.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:921b25618b084e75d424a9f8e6403bfeb7abef074bb6c3174701e0f2542debcf"}, + {file = "grpcio-1.73.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:277b426a0ed341e8447fbf6c1d6b68c952adddf585ea4685aa563de0f03df887"}, + {file = "grpcio-1.73.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:96c112333309493c10e118d92f04594f9055774757f5d101b39f8150f8c25582"}, + {file = "grpcio-1.73.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f48e862aed925ae987eb7084409a80985de75243389dc9d9c271dd711e589918"}, + {file = "grpcio-1.73.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83a6c2cce218e28f5040429835fa34a29319071079e3169f9543c3fbeff166d2"}, + {file = "grpcio-1.73.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:65b0458a10b100d815a8426b1442bd17001fdb77ea13665b2f7dc9e8587fdc6b"}, + {file = "grpcio-1.73.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0a9f3ea8dce9eae9d7cb36827200133a72b37a63896e0e61a9d5ec7d61a59ab1"}, + {file = "grpcio-1.73.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:de18769aea47f18e782bf6819a37c1c528914bfd5683b8782b9da356506190c8"}, + {file = "grpcio-1.73.1-cp312-cp312-win32.whl", hash = "sha256:24e06a5319e33041e322d32c62b1e728f18ab8c9dbc91729a3d9f9e3ed336642"}, + {file = "grpcio-1.73.1-cp312-cp312-win_amd64.whl", hash = "sha256:303c8135d8ab176f8038c14cc10d698ae1db9c480f2b2823f7a987aa2a4c5646"}, + {file = "grpcio-1.73.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b310824ab5092cf74750ebd8a8a8981c1810cb2b363210e70d06ef37ad80d4f9"}, + {file = "grpcio-1.73.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:8f5a6df3fba31a3485096ac85b2e34b9666ffb0590df0cd044f58694e6a1f6b5"}, + {file = "grpcio-1.73.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:052e28fe9c41357da42250a91926a3e2f74c046575c070b69659467ca5aa976b"}, + {file = "grpcio-1.73.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c0bf15f629b1497436596b1cbddddfa3234273490229ca29561209778ebe182"}, + {file = "grpcio-1.73.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab860d5bfa788c5a021fba264802e2593688cd965d1374d31d2b1a34cacd854"}, + {file = "grpcio-1.73.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ad1d958c31cc91ab050bd8a91355480b8e0683e21176522bacea225ce51163f2"}, + {file = "grpcio-1.73.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f43ffb3bd415c57224c7427bfb9e6c46a0b6e998754bfa0d00f408e1873dcbb5"}, + {file = "grpcio-1.73.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:686231cdd03a8a8055f798b2b54b19428cdf18fa1549bee92249b43607c42668"}, + {file = "grpcio-1.73.1-cp313-cp313-win32.whl", hash = "sha256:89018866a096e2ce21e05eabed1567479713ebe57b1db7cbb0f1e3b896793ba4"}, + {file = "grpcio-1.73.1-cp313-cp313-win_amd64.whl", hash = "sha256:4a68f8c9966b94dff693670a5cf2b54888a48a5011c5d9ce2295a1a1465ee84f"}, + {file = "grpcio-1.73.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:b4adc97d2d7f5c660a5498bda978ebb866066ad10097265a5da0511323ae9f50"}, + {file = "grpcio-1.73.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:c45a28a0cfb6ddcc7dc50a29de44ecac53d115c3388b2782404218db51cb2df3"}, + {file = "grpcio-1.73.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:10af9f2ab98a39f5b6c1896c6fc2036744b5b41d12739d48bed4c3e15b6cf900"}, + {file = "grpcio-1.73.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45cf17dcce5ebdb7b4fe9e86cb338fa99d7d1bb71defc78228e1ddf8d0de8cbb"}, + {file = "grpcio-1.73.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c502c2e950fc7e8bf05c047e8a14522ef7babac59abbfde6dbf46b7a0d9c71e"}, + {file = "grpcio-1.73.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6abfc0f9153dc4924536f40336f88bd4fe7bd7494f028675e2e04291b8c2c62a"}, + {file = "grpcio-1.73.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ed451a0e39c8e51eb1612b78686839efd1a920666d1666c1adfdb4fd51680c0f"}, + {file = "grpcio-1.73.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:07f08705a5505c9b5b0cbcbabafb96462b5a15b7236bbf6bbcc6b0b91e1cbd7e"}, + {file = "grpcio-1.73.1-cp39-cp39-win32.whl", hash = "sha256:ad5c958cc3d98bb9d71714dc69f1c13aaf2f4b53e29d4cc3f1501ef2e4d129b2"}, + {file = "grpcio-1.73.1-cp39-cp39-win_amd64.whl", hash = "sha256:42f0660bce31b745eb9d23f094a332d31f210dcadd0fc8e5be7e4c62a87ce86b"}, + {file = "grpcio-1.73.1.tar.gz", hash = "sha256:7fce2cd1c0c1116cf3850564ebfc3264fba75d3c74a7414373f1238ea365ef87"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.66.2)"] +protobuf = ["grpcio-tools (>=1.73.1)"] [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] [[package]] name = "httpcore" -version = "1.0.5" +version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, - {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, ] [package.dependencies] certifi = "*" -h11 = ">=0.13,<0.15" +h11 = ">=0.16" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.26.0)"] +trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.27.2" +version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, - {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] @@ -1252,10 +1375,9 @@ anyio = "*" certifi = "*" httpcore = "==1.*" idna = "*" -sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1267,6 +1389,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "docs", "test"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1281,6 +1404,7 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["docs"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -1288,36 +1412,38 @@ files = [ [[package]] name = "importlib-metadata" -version = "8.5.0" +version = "8.7.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] [package.dependencies] zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -1326,6 +1452,7 @@ version = "6.29.5" description = "IPython Kernel for Jupyter" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, @@ -1355,40 +1482,51 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio [[package]] name = "ipython" -version = "8.18.1" +version = "9.4.0" description = "IPython: Productive Interactive Computing" optional = false -python-versions = ">=3.9" +python-versions = ">=3.11" +groups = ["main", "test"] files = [ - {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, - {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, + {file = "ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066"}, + {file = "ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +ipython-pygments-lexers = "*" jedi = ">=0.16" matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -prompt-toolkit = ">=3.0.41,<3.1.0" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt_toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} +stack_data = "*" +traitlets = ">=5.13.0" +typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} [package.extras] -all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +all = ["ipython[doc,matplotlib,test,test-extra]"] black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] -kernel = ["ipykernel"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["ipywidgets", "notebook"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"] +matplotlib = ["matplotlib"] +test = ["packaging", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipykernel", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbclient", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +description = "Defines a variety of Pygments lexers for highlighting IPython code." +optional = false +python-versions = ">=3.8" +groups = ["main", "test"] +files = [ + {file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"}, + {file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"}, +] + +[package.dependencies] +pygments = "*" [[package]] name = "ipywidgets" @@ -1396,6 +1534,7 @@ version = "8.1.2" description = "Jupyter interactive widgets" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "ipywidgets-8.1.2-py3-none-any.whl", hash = "sha256:bbe43850d79fb5e906b14801d6c01402857996864d1e5b6fa62dd2ee35559f60"}, {file = "ipywidgets-8.1.2.tar.gz", hash = "sha256:d0b9b41e49bae926a866e613a39b0f0097745d2b9f1f3dd406641b4a57ec42c9"}, @@ -1417,6 +1556,7 @@ version = "20.11.0" description = "Operations with ISO 8601 durations" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, @@ -1427,32 +1567,34 @@ arrow = ">=0.15.0" [[package]] name = "jedi" -version = "0.19.1" +version = "0.19.2" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.6" +groups = ["main", "test"] files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, ] [package.dependencies] -parso = ">=0.8.3,<0.9.0" +parso = ">=0.8.4,<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)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["dev", "docs", "test"] files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -1463,21 +1605,26 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "json5" -version = "0.9.25" +version = "0.12.0" description = "A Python implementation of the JSON5 data format." optional = false -python-versions = ">=3.8" +python-versions = ">=3.8.0" +groups = ["test"] files = [ - {file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, - {file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, + {file = "json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db"}, + {file = "json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a"}, ] +[package.extras] +dev = ["build (==1.2.2.post1)", "coverage (==7.5.4) ; python_version < \"3.9\"", "coverage (==7.8.0) ; python_version >= \"3.9\"", "mypy (==1.14.1) ; python_version < \"3.9\"", "mypy (==1.15.0) ; python_version >= \"3.9\"", "pip (==25.0.1)", "pylint (==3.2.7) ; python_version < \"3.9\"", "pylint (==3.3.6) ; python_version >= \"3.9\"", "ruff (==0.11.2)", "twine (==6.1.0)", "uv (==0.6.11)"] + [[package]] name = "jsonpointer" version = "3.0.0" description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, @@ -1485,13 +1632,14 @@ files = [ [[package]] name = "jsonschema" -version = "4.23.0" +version = "4.24.0" description = "An implementation of JSON Schema validation for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test"] files = [ - {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, - {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, + {file = "jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d"}, + {file = "jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196"}, ] [package.dependencies] @@ -1514,13 +1662,14 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "jsonschema-specifications" -version = "2023.12.1" +version = "2025.4.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test"] files = [ - {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, - {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, + {file = "jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af"}, + {file = "jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"}, ] [package.dependencies] @@ -1532,13 +1681,13 @@ version = "8.6.3" description = "Jupyter protocol implementation and client libraries" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" python-dateutil = ">=2.8.2" pyzmq = ">=23.0" @@ -1547,17 +1696,18 @@ traitlets = ">=5.3" [package.extras] docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko ; sys_platform == \"win32\"", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] [[package]] name = "jupyter-core" -version = "5.7.2" +version = "5.8.1" description = "Jupyter core package. A base package on which Jupyter projects rely." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, - {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, + {file = "jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0"}, + {file = "jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941"}, ] [package.dependencies] @@ -1566,22 +1716,24 @@ pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_ traitlets = ">=5.3" [package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] -test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<9)", "pytest-cov", "pytest-timeout"] [[package]] name = "jupyter-events" -version = "0.10.0" +version = "0.12.0" description = "Jupyter Event System library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["test"] files = [ - {file = "jupyter_events-0.10.0-py3-none-any.whl", hash = "sha256:4b72130875e59d57716d327ea70d3ebc3af1944d3717e5a498b8a06c6c159960"}, - {file = "jupyter_events-0.10.0.tar.gz", hash = "sha256:670b8229d3cc882ec782144ed22e0d29e1c2d639263f92ca8383e66682845e22"}, + {file = "jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb"}, + {file = "jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b"}, ] [package.dependencies] jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} +packaging = "*" python-json-logger = ">=2.0.4" pyyaml = ">=5.3" referencing = "*" @@ -1591,7 +1743,7 @@ traitlets = ">=5.3" [package.extras] cli = ["click", "rich"] -docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"] +docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8)", "sphinxcontrib-spelling"] test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] [[package]] @@ -1600,24 +1752,25 @@ version = "2.2.5" description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001"}, {file = "jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jupyter-server = ">=1.1.2" [[package]] name = "jupyter-server" -version = "2.14.2" +version = "2.16.0" description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["test"] files = [ - {file = "jupyter_server-2.14.2-py3-none-any.whl", hash = "sha256:47ff506127c2f7851a17bf4713434208fc490955d0e8632e95014a9a9afbeefd"}, - {file = "jupyter_server-2.14.2.tar.gz", hash = "sha256:66095021aa9638ced276c248b1d81862e4c50f292d575920bbe960de1c56b12b"}, + {file = "jupyter_server-2.16.0-py3-none-any.whl", hash = "sha256:3d8db5be3bc64403b1c65b400a1d7f4647a5ce743f3b20dbdefe8ddb7b55af9e"}, + {file = "jupyter_server-2.16.0.tar.gz", hash = "sha256:65d4b44fdf2dcbbdfe0aa1ace4a842d4aaf746a2b7b168134d5aaed35621b7f6"}, ] [package.dependencies] @@ -1626,7 +1779,7 @@ argon2-cffi = ">=21.1" jinja2 = ">=3.0.3" jupyter-client = ">=7.4.4" jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -jupyter-events = ">=0.9.0" +jupyter-events = ">=0.11.0" jupyter-server-terminals = ">=0.4.4" nbconvert = ">=6.4.4" nbformat = ">=5.3.0" @@ -1651,6 +1804,7 @@ version = "0.5.3" description = "A Jupyter Server Extension Providing Terminals." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa"}, {file = "jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269"}, @@ -1670,6 +1824,7 @@ version = "4.3.1" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "jupyterlab-4.3.1-py3-none-any.whl", hash = "sha256:2d9a1c305bc748e277819a17a5d5e22452e533e835f4237b2f30f3b0e491e01f"}, {file = "jupyterlab-4.3.1.tar.gz", hash = "sha256:a4a338327556443521731d82f2a6ccf926df478914ca029616621704d47c3c65"}, @@ -1678,7 +1833,6 @@ files = [ [package.dependencies] async-lru = ">=1.0.0" httpx = ">=0.25.0" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} ipykernel = ">=6.5.0" jinja2 = ">=3.0.3" jupyter-core = "*" @@ -1688,7 +1842,6 @@ jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2" packaging = "*" setuptools = ">=40.1.0" -tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} tornado = ">=6.2.0" traitlets = "*" @@ -1705,6 +1858,7 @@ version = "0.3.0" description = "Pygments theme using JupyterLab CSS variables" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, @@ -1716,6 +1870,7 @@ version = "2.27.3" description = "A set of server components for JupyterLab and JupyterLab like applications." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4"}, {file = "jupyterlab_server-2.27.3.tar.gz", hash = "sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4"}, @@ -1723,7 +1878,6 @@ files = [ [package.dependencies] babel = ">=2.10" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jinja2 = ">=3.0.3" json5 = ">=0.9.0" jsonschema = ">=4.18.0" @@ -1738,34 +1892,37 @@ test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-v [[package]] name = "jupyterlab-widgets" -version = "3.0.13" +version = "3.0.15" description = "Jupyter interactive widgets for JupyterLab" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "jupyterlab_widgets-3.0.13-py3-none-any.whl", hash = "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54"}, - {file = "jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed"}, + {file = "jupyterlab_widgets-3.0.15-py3-none-any.whl", hash = "sha256:d59023d7d7ef71400d51e6fee9a88867f6e65e10a4201605d2d7f3e8f012a31c"}, + {file = "jupyterlab_widgets-3.0.15.tar.gz", hash = "sha256:2920888a0c2922351a9202817957a68c07d99673504d6cd37345299e971bb08b"}, ] [[package]] name = "kubernetes" -version = "26.1.0" +version = "33.1.0" description = "Kubernetes python client" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "kubernetes-26.1.0-py2.py3-none-any.whl", hash = "sha256:e3db6800abf7e36c38d2629b5cb6b74d10988ee0cba6fba45595a7cbe60c0042"}, - {file = "kubernetes-26.1.0.tar.gz", hash = "sha256:5854b0c508e8d217ca205591384ab58389abdae608576f9c9afc35a3c76a366c"}, + {file = "kubernetes-33.1.0-py2.py3-none-any.whl", hash = "sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5"}, + {file = "kubernetes-33.1.0.tar.gz", hash = "sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993"}, ] [package.dependencies] certifi = ">=14.05.14" +durationpy = ">=0.7" google-auth = ">=1.0.1" +oauthlib = ">=3.2.2" python-dateutil = ">=2.5.3" pyyaml = ">=5.4.1" requests = "*" requests-oauthlib = "*" -setuptools = ">=21.0.0" six = ">=1.9.0" urllib3 = ">=1.24.2" websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" @@ -1779,6 +1936,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -1799,71 +1957,73 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +python-versions = ">=3.9" +groups = ["dev", "docs", "test"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] @@ -1872,6 +2032,7 @@ version = "0.1.7" description = "Inline Matplotlib backend for Jupyter" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, @@ -1886,263 +2047,223 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] -[[package]] -name = "memray" -version = "1.10.0" -description = "A memory profiler for Python applications" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "memray-1.10.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:843a688877691746f9d1835cfa8a65139948471bdd78720435808d20bc30a1cc"}, - {file = "memray-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6937d7ef67d18ccc01c3250cdf3b4ef1445b859ee8756f09e3d11bd3ff0c7d67"}, - {file = "memray-1.10.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:23e8c402625cfb32d0e9edb5ec0945f3e5e54bc6b0c5699f6284302082b80bd4"}, - {file = "memray-1.10.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f16c5c8730b616613dc8bafe32649ca6bd7252606251eb00148582011758d0b5"}, - {file = "memray-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7aeb47174c42e99740a8e2b3b6fe0932c95d987258d48a746974ead19176c26"}, - {file = "memray-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2ce59ef485db3634de98b3a026d2450fc0a875e3a58a9ea85f7a89098841defe"}, - {file = "memray-1.10.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:53a8f66af18b1f3bcf5c9f3c95ae4134dd675903a38f9d0e6341b7bca01b63d0"}, - {file = "memray-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9627184c926252c8f719c301f1fefe970f0d033c643a6448b93fed2889d1ea94"}, - {file = "memray-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3a14960838d89a91747885897d34134afb65883cc3b0ed7ff30fe1af00f9fe6"}, - {file = "memray-1.10.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f2a47871c172a0539bd72737bb6b294fc10c510464066b825d90fcd3bb4916"}, - {file = "memray-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c401c57f49c4c5f1fecaee1e746f537cdc6680da05fb963dc143bd08ee109bf"}, - {file = "memray-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ce22a887a585ef5020896de89ffc793e531b65ccc81fbafcc7886010c2c562b3"}, - {file = "memray-1.10.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:b75040f28e8678d0e9c4907d55c95cf26db8ef5adc9941a228f1b280a9efd9c0"}, - {file = "memray-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:95e563d9c976e429ad597ad2720d95cebbe8bac891a3082465439143e2740772"}, - {file = "memray-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:663d463e89a64bae4a6b2f8c837d11a3d094834442d536a4165e1d31899a3500"}, - {file = "memray-1.10.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a21745fb516b7a6efcd40aa7487c59e9313fcfc782d0193fcfcf00b48426874"}, - {file = "memray-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6d683c4f8d25c6ad06ae18715f218983c5eb86803953615e902d632fdf6ec1"}, - {file = "memray-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b311e91203be71e1a0ce5e4f978137765bcb1045f3bf5646129c83c5b96ab3c"}, - {file = "memray-1.10.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:68bd8df023c8a32f44c11d997e5c536837e27c0955daf557d3a377edd55a1dd3"}, - {file = "memray-1.10.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:322ed0b69014a0969b777768d461a785203f81f9864386b666b5b26645d9c294"}, - {file = "memray-1.10.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e985fb7646b0475c303919d19211d2aa54e5a9e2cd2a102472299be5dbebd3"}, - {file = "memray-1.10.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4eba29179772b4a2e440a065b320b03bc2e73fe2648bdf7936aa3b9a086fab4a"}, - {file = "memray-1.10.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:b681519357d94f5f0857fbc6029e7c44d3f41436109e955a14fd312d8317bc35"}, - {file = "memray-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8196c684f1be8fe423e5cdd2356d4255a2cb482a1f3e89612b70d2a2862cf5bb"}, - {file = "memray-1.10.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:898acd60f57a10dc5aaf1fd64aa2f821f0420114f3f60c3058083788603f173a"}, - {file = "memray-1.10.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6fd13ef666c7fced9768d1cfabf71dc6dfa6724935a8dff463495ac2dc5e13a4"}, - {file = "memray-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e356af93e3b031c83957e9ac1a653f5aaba5df1e357dd17142f5ed19bb3dc660"}, - {file = "memray-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92c372cb262eddd23049f945ca9527f0e4cc7c40a070aade1802d066f680885b"}, - {file = "memray-1.10.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:38393c86ce6d0a08e6ec0eb1401d49803b7c0c950c2565386751cdc81568cba8"}, - {file = "memray-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a8bb7fbd8303c4f0017ba7faef6b88f904cda2931ed667cbf3b98f024b3bc44"}, - {file = "memray-1.10.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d56f37a34125684746c13d24bd7a3fb17549b0bb355eb50969eb11e05e3ba62"}, - {file = "memray-1.10.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85c32d6613d81b075f740e398c4d653e0803cd48e82c33dcd584c109d6782666"}, - {file = "memray-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:566602b2143e06b3d592901d98c52ce4599e71aa2555146eeb5cec03506f9498"}, - {file = "memray-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:391aac6c9f744528d3186bc82d708a1acc83525778f804045d7c96f860f8ec98"}, - {file = "memray-1.10.0.tar.gz", hash = "sha256:38322e052b882790993412f1840517a51818aa55c47037f69915b2007f2c4cee"}, -] - -[package.dependencies] -jinja2 = ">=2.9" -rich = ">=11.2.0" - -[package.extras] -benchmark = ["asv"] -dev = ["Cython", "IPython", "asv", "black", "bump2version", "check-manifest", "flake8", "furo", "greenlet", "ipython", "isort", "mypy", "pytest", "pytest-cov", "setuptools", "sphinx", "sphinx-argparse", "towncrier"] -docs = ["IPython", "bump2version", "furo", "sphinx", "sphinx-argparse", "towncrier"] -lint = ["black", "check-manifest", "flake8", "isort", "mypy"] -test = ["Cython", "greenlet", "ipython", "pytest", "pytest-cov", "setuptools"] - [[package]] name = "mistune" -version = "3.0.2" +version = "3.1.3" description = "A sane and fast Markdown parser with useful plugins and renderers" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, - {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, + {file = "mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9"}, + {file = "mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0"}, ] [[package]] name = "msgpack" -version = "1.1.0" +version = "1.1.1" description = "MessagePack serializer" optional = false python-versions = ">=3.8" -files = [ - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, - {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, - {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, - {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, - {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, - {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, - {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, - {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, - {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, - {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, - {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, - {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, - {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, - {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, +groups = ["main"] +files = [ + {file = "msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed"}, + {file = "msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4"}, + {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75"}, + {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338"}, + {file = "msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd"}, + {file = "msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8"}, + {file = "msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558"}, + {file = "msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f"}, + {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2"}, + {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752"}, + {file = "msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295"}, + {file = "msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458"}, + {file = "msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238"}, + {file = "msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a"}, + {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef"}, + {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a"}, + {file = "msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c"}, + {file = "msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4"}, + {file = "msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0"}, + {file = "msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a"}, + {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7"}, + {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5"}, + {file = "msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323"}, + {file = "msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba1be28247e68994355e028dcd668316db30c1f758d3241a7b903ac78dcd285"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f93dcddb243159c9e4109c9750ba5b335ab8d48d9522c5308cd05d7e3ce600"}, + {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fbbc0b906a24038c9958a1ba7ae0918ad35b06cb449d398b76a7d08470b0ed9"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:61e35a55a546a1690d9d09effaa436c25ae6130573b6ee9829c37ef0f18d5e78"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:1abfc6e949b352dadf4bce0eb78023212ec5ac42f6abfd469ce91d783c149c2a"}, + {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:996f2609ddf0142daba4cefd767d6db26958aac8439ee41db9cc0db9f4c4c3a6"}, + {file = "msgpack-1.1.1-cp38-cp38-win32.whl", hash = "sha256:4d3237b224b930d58e9d83c81c0dba7aacc20fcc2f89c1e5423aa0529a4cd142"}, + {file = "msgpack-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:da8f41e602574ece93dbbda1fab24650d6bf2a24089f9e9dbb4f5730ec1e58ad"}, + {file = "msgpack-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b"}, + {file = "msgpack-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf"}, + {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88"}, + {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478"}, + {file = "msgpack-1.1.1-cp39-cp39-win32.whl", hash = "sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57"}, + {file = "msgpack-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084"}, + {file = "msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd"}, ] [[package]] name = "multidict" -version = "6.1.0" +version = "6.6.3" description = "multidict implementation" optional = false -python-versions = ">=3.8" -files = [ - {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, - {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, - {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, - {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, - {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, - {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, - {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, - {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, - {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, - {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, - {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, - {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, - {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, - {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, - {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, - {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, - {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817"}, + {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140"}, + {file = "multidict-6.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd7793bab517e706c9ed9d7310b06c8672fd0aeee5781bfad612f56b8e0f7d14"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:72d8815f2cd3cf3df0f83cac3f3ef801d908b2d90409ae28102e0553af85545a"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:531e331a2ee53543ab32b16334e2deb26f4e6b9b28e41f8e0c87e99a6c8e2d69"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:42ca5aa9329a63be8dc49040f63817d1ac980e02eeddba763a9ae5b4027b9c9c"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:208b9b9757060b9faa6f11ab4bc52846e4f3c2fb8b14d5680c8aac80af3dc751"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:acf6b97bd0884891af6a8b43d0f586ab2fcf8e717cbd47ab4bdddc09e20652d8"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68e9e12ed00e2089725669bdc88602b0b6f8d23c0c95e52b95f0bc69f7fe9b55"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05db2f66c9addb10cfa226e1acb363450fab2ff8a6df73c622fefe2f5af6d4e7"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0db58da8eafb514db832a1b44f8fa7906fdd102f7d982025f816a93ba45e3dcb"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14117a41c8fdb3ee19c743b1c027da0736fdb79584d61a766da53d399b71176c"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:877443eaaabcd0b74ff32ebeed6f6176c71850feb7d6a1d2db65945256ea535c"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:70b72e749a4f6e7ed8fb334fa8d8496384840319512746a5f42fa0aec79f4d61"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43571f785b86afd02b3855c5ac8e86ec921b760298d6f82ff2a61daf5a35330b"}, + {file = "multidict-6.6.3-cp310-cp310-win32.whl", hash = "sha256:20c5a0c3c13a15fd5ea86c42311859f970070e4e24de5a550e99d7c271d76318"}, + {file = "multidict-6.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab0a34a007704c625e25a9116c6770b4d3617a071c8a7c30cd338dfbadfe6485"}, + {file = "multidict-6.6.3-cp310-cp310-win_arm64.whl", hash = "sha256:769841d70ca8bdd140a715746199fc6473414bd02efd678d75681d2d6a8986c5"}, + {file = "multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c"}, + {file = "multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df"}, + {file = "multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183"}, + {file = "multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5"}, + {file = "multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2"}, + {file = "multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb"}, + {file = "multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6"}, + {file = "multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f"}, + {file = "multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10"}, + {file = "multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5"}, + {file = "multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17"}, + {file = "multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b"}, + {file = "multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55"}, + {file = "multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b"}, + {file = "multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6"}, + {file = "multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e"}, + {file = "multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9"}, + {file = "multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600"}, + {file = "multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134"}, + {file = "multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37"}, + {file = "multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c"}, + {file = "multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e"}, + {file = "multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d"}, + {file = "multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb"}, + {file = "multidict-6.6.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c8161b5a7778d3137ea2ee7ae8a08cce0010de3b00ac671c5ebddeaa17cefd22"}, + {file = "multidict-6.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1328201ee930f069961ae707d59c6627ac92e351ed5b92397cf534d1336ce557"}, + {file = "multidict-6.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b1db4d2093d6b235de76932febf9d50766cf49a5692277b2c28a501c9637f616"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53becb01dd8ebd19d1724bebe369cfa87e4e7f29abbbe5c14c98ce4c383e16cd"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41bb9d1d4c303886e2d85bade86e59885112a7f4277af5ad47ab919a2251f306"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:775b464d31dac90f23192af9c291dc9f423101857e33e9ebf0020a10bfcf4144"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d04d01f0a913202205a598246cf77826fe3baa5a63e9f6ccf1ab0601cf56eca0"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d25594d3b38a2e6cabfdcafef339f754ca6e81fbbdb6650ad773ea9775af35ab"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35712f1748d409e0707b165bf49f9f17f9e28ae85470c41615778f8d4f7d9609"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1c8082e5814b662de8589d6a06c17e77940d5539080cbab9fe6794b5241b76d9"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:61af8a4b771f1d4d000b3168c12c3120ccf7284502a94aa58c68a81f5afac090"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:448e4a9afccbf297577f2eaa586f07067441e7b63c8362a3540ba5a38dc0f14a"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:233ad16999afc2bbd3e534ad8dbe685ef8ee49a37dbc2cdc9514e57b6d589ced"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:bb933c891cd4da6bdcc9733d048e994e22e1883287ff7540c2a0f3b117605092"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:37b09ca60998e87734699e88c2363abfd457ed18cfbf88e4009a4e83788e63ed"}, + {file = "multidict-6.6.3-cp39-cp39-win32.whl", hash = "sha256:f54cb79d26d0cd420637d184af38f0668558f3c4bbe22ab7ad830e67249f2e0b"}, + {file = "multidict-6.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:295adc9c0551e5d5214b45cf29ca23dbc28c2d197a9c30d51aed9e037cb7c578"}, + {file = "multidict-6.6.3-cp39-cp39-win_arm64.whl", hash = "sha256:15332783596f227db50fb261c2c251a58ac3873c457f3a550a95d5c0aa3c770d"}, + {file = "multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a"}, + {file = "multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} - [[package]] name = "nbclient" -version = "0.10.0" +version = "0.10.2" description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" +groups = ["test"] files = [ - {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, - {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, + {file = "nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d"}, + {file = "nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193"}, ] [package.dependencies] @@ -2153,25 +2274,25 @@ traitlets = ">=5.4" [package.extras] dev = ["pre-commit"] -docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] -test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] +docs = ["autodoc-traits", "flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "mock", "moto", "myst-parser", "nbconvert (>=7.1.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling", "testpath", "xmltodict"] +test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.1.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] [[package]] name = "nbconvert" -version = "7.16.4" +version = "7.16.6" description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"}, - {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"}, + {file = "nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b"}, + {file = "nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582"}, ] [package.dependencies] beautifulsoup4 = "*" -bleach = "!=5.0.0" +bleach = {version = "!=5.0.0", extras = ["css"]} defusedxml = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} jinja2 = ">=3.0" jupyter-core = ">=4.7" jupyterlab-pygments = "*" @@ -2182,7 +2303,6 @@ nbformat = ">=5.7" packaging = "*" pandocfilters = ">=1.4.1" pygments = ">=2.4.1" -tinycss2 = "*" traitlets = ">=5.1" [package.extras] @@ -2200,6 +2320,7 @@ version = "5.10.4" description = "The Jupyter Notebook format" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, @@ -2221,6 +2342,7 @@ version = "1.6.0" description = "Patch asyncio to allow nested event loops" optional = false python-versions = ">=3.5" +groups = ["test"] files = [ {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, @@ -2232,6 +2354,7 @@ version = "0.2.4" description = "A shim layer for notebook traits and config" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"}, @@ -2245,67 +2368,75 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync" [[package]] name = "numpy" -version = "2.0.2" +version = "2.3.1" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, - {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, - {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, - {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, - {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, - {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, - {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, - {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, - {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, - {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "numpy-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ea9e48336a402551f52cd8f593343699003d2353daa4b72ce8d34f66b722070"}, + {file = "numpy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccb7336eaf0e77c1635b232c141846493a588ec9ea777a7c24d7166bb8533ae"}, + {file = "numpy-2.3.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bb3a4a61e1d327e035275d2a993c96fa786e4913aa089843e6a2d9dd205c66a"}, + {file = "numpy-2.3.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:e344eb79dab01f1e838ebb67aab09965fb271d6da6b00adda26328ac27d4a66e"}, + {file = "numpy-2.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:467db865b392168ceb1ef1ffa6f5a86e62468c43e0cfb4ab6da667ede10e58db"}, + {file = "numpy-2.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:afed2ce4a84f6b0fc6c1ce734ff368cbf5a5e24e8954a338f3bdffa0718adffb"}, + {file = "numpy-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0025048b3c1557a20bc80d06fdeb8cc7fc193721484cca82b2cfa072fec71a93"}, + {file = "numpy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5ee121b60aa509679b682819c602579e1df14a5b07fe95671c8849aad8f2115"}, + {file = "numpy-2.3.1-cp311-cp311-win32.whl", hash = "sha256:a8b740f5579ae4585831b3cf0e3b0425c667274f82a484866d2adf9570539369"}, + {file = "numpy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4580adadc53311b163444f877e0789f1c8861e2698f6b2a4ca852fda154f3ff"}, + {file = "numpy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:ec0bdafa906f95adc9a0c6f26a4871fa753f25caaa0e032578a30457bff0af6a"}, + {file = "numpy-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2959d8f268f3d8ee402b04a9ec4bb7604555aeacf78b360dc4ec27f1d508177d"}, + {file = "numpy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:762e0c0c6b56bdedfef9a8e1d4538556438288c4276901ea008ae44091954e29"}, + {file = "numpy-2.3.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:867ef172a0976aaa1f1d1b63cf2090de8b636a7674607d514505fb7276ab08fc"}, + {file = "numpy-2.3.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4e602e1b8682c2b833af89ba641ad4176053aaa50f5cacda1a27004352dde943"}, + {file = "numpy-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8e333040d069eba1652fb08962ec5b76af7f2c7bce1df7e1418c8055cf776f25"}, + {file = "numpy-2.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e7cbf5a5eafd8d230a3ce356d892512185230e4781a361229bd902ff403bc660"}, + {file = "numpy-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1b8f26d1086835f442286c1d9b64bb3974b0b1e41bb105358fd07d20872952"}, + {file = "numpy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee8340cb48c9b7a5899d1149eece41ca535513a9698098edbade2a8e7a84da77"}, + {file = "numpy-2.3.1-cp312-cp312-win32.whl", hash = "sha256:e772dda20a6002ef7061713dc1e2585bc1b534e7909b2030b5a46dae8ff077ab"}, + {file = "numpy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfecc7822543abdea6de08758091da655ea2210b8ffa1faf116b940693d3df76"}, + {file = "numpy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:7be91b2239af2658653c5bb6f1b8bccafaf08226a258caf78ce44710a0160d30"}, + {file = "numpy-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25a1992b0a3fdcdaec9f552ef10d8103186f5397ab45e2d25f8ac51b1a6b97e8"}, + {file = "numpy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dea630156d39b02a63c18f508f85010230409db5b2927ba59c8ba4ab3e8272e"}, + {file = "numpy-2.3.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bada6058dd886061f10ea15f230ccf7dfff40572e99fef440a4a857c8728c9c0"}, + {file = "numpy-2.3.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:a894f3816eb17b29e4783e5873f92faf55b710c2519e5c351767c51f79d8526d"}, + {file = "numpy-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:18703df6c4a4fee55fd3d6e5a253d01c5d33a295409b03fda0c86b3ca2ff41a1"}, + {file = "numpy-2.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5902660491bd7a48b2ec16c23ccb9124b8abfd9583c5fdfa123fe6b421e03de1"}, + {file = "numpy-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36890eb9e9d2081137bd78d29050ba63b8dab95dff7912eadf1185e80074b2a0"}, + {file = "numpy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a780033466159c2270531e2b8ac063704592a0bc62ec4a1b991c7c40705eb0e8"}, + {file = "numpy-2.3.1-cp313-cp313-win32.whl", hash = "sha256:39bff12c076812595c3a306f22bfe49919c5513aa1e0e70fac756a0be7c2a2b8"}, + {file = "numpy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d5ee6eec45f08ce507a6570e06f2f879b374a552087a4179ea7838edbcbfa42"}, + {file = "numpy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c4d9e0a8368db90f93bd192bfa771ace63137c3488d198ee21dfb8e7771916e"}, + {file = "numpy-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b0b5397374f32ec0649dd98c652a1798192042e715df918c20672c62fb52d4b8"}, + {file = "numpy-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c5bdf2015ccfcee8253fb8be695516ac4457c743473a43290fd36eba6a1777eb"}, + {file = "numpy-2.3.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d70f20df7f08b90a2062c1f07737dd340adccf2068d0f1b9b3d56e2038979fee"}, + {file = "numpy-2.3.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:2fb86b7e58f9ac50e1e9dd1290154107e47d1eef23a0ae9145ded06ea606f992"}, + {file = "numpy-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:23ab05b2d241f76cb883ce8b9a93a680752fbfcbd51c50eff0b88b979e471d8c"}, + {file = "numpy-2.3.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ce2ce9e5de4703a673e705183f64fd5da5bf36e7beddcb63a25ee2286e71ca48"}, + {file = "numpy-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c4913079974eeb5c16ccfd2b1f09354b8fed7e0d6f2cab933104a09a6419b1ee"}, + {file = "numpy-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:010ce9b4f00d5c036053ca684c77441f2f2c934fd23bee058b4d6f196efd8280"}, + {file = "numpy-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:6269b9edfe32912584ec496d91b00b6d34282ca1d07eb10e82dfc780907d6c2e"}, + {file = "numpy-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2a809637460e88a113e186e87f228d74ae2852a2e0c44de275263376f17b5bdc"}, + {file = "numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244"}, + {file = "numpy-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad506d4b09e684394c42c966ec1527f6ebc25da7f4da4b1b056606ffe446b8a3"}, + {file = "numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ebb8603d45bc86bbd5edb0d63e52c5fd9e7945d3a503b77e486bd88dde67a19b"}, + {file = "numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:15aa4c392ac396e2ad3d0a2680c0f0dee420f9fed14eef09bdb9450ee6dcb7b7"}, + {file = "numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c6e0bf9d1a2f50d2b65a7cf56db37c095af17b59f6c132396f7c6d5dd76484df"}, + {file = "numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eabd7e8740d494ce2b4ea0ff05afa1b7b291e978c0ae075487c51e8bd93c0c68"}, + {file = "numpy-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e610832418a2bc09d974cc9fecebfa51e9532d6190223bc5ef6a7402ebf3b5cb"}, + {file = "numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b"}, ] [[package]] name = "oauthlib" -version = "3.2.2" +version = "3.3.1" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, - {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, + {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, + {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, ] [package.extras] @@ -2319,6 +2450,7 @@ version = "0.11.4" description = "A stats collection and distributed tracing framework" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "opencensus-0.11.4-py2.py3-none-any.whl", hash = "sha256:a18487ce68bc19900336e0ff4655c5a116daf10c1b3685ece8d971bddad6a864"}, {file = "opencensus-0.11.4.tar.gz", hash = "sha256:cbef87d8b8773064ab60e5c2a1ced58bbaa38a6d052c41aec224958ce544eff2"}, @@ -2335,6 +2467,7 @@ version = "0.1.3" description = "OpenCensus Runtime Context" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "opencensus-context-0.1.3.tar.gz", hash = "sha256:a03108c3c10d8c80bb5ddf5c8a1f033161fa61972a9917f9b9b3a18517f0088c"}, {file = "opencensus_context-0.1.3-py2.py3-none-any.whl", hash = "sha256:073bb0590007af276853009fac7e4bab1d523c3f03baf4cb4511ca38967c6039"}, @@ -2346,6 +2479,7 @@ version = "1.0.18" description = "OpenShift python client" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "openshift-client-1.0.18.tar.gz", hash = "sha256:be3979440cfd96788146a3a1650dabe939d4d516eea0b39f87e66d2ab39495b1"}, {file = "openshift_client-1.0.18-py2.py3-none-any.whl", hash = "sha256:d8a84080307ccd9556f6c62a3707a3e6507baedee36fa425754f67db9ded528b"}, @@ -2356,12 +2490,94 @@ paramiko = "*" pyyaml = "*" six = "*" +[[package]] +name = "opentelemetry-api" +version = "1.34.1" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c"}, + {file = "opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3"}, +] + +[package.dependencies] +importlib-metadata = ">=6.0,<8.8.0" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-exporter-prometheus" +version = "0.55b1" +description = "Prometheus Metric Exporter for OpenTelemetry" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_prometheus-0.55b1-py3-none-any.whl", hash = "sha256:f364fbbff9e5de37a112ff104d1185fb1d7e2046c5ab5911e5afebc7ab3ddf0e"}, + {file = "opentelemetry_exporter_prometheus-0.55b1.tar.gz", hash = "sha256:d13ec0b22bf394113ff1ada5da98133a4b051779b803dae183188e26c4bd9ee0"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-sdk = ">=1.34.1,<1.35.0" +prometheus-client = ">=0.5.0,<1.0.0" + +[[package]] +name = "opentelemetry-proto" +version = "1.11.1" +description = "OpenTelemetry Python Proto" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "opentelemetry-proto-1.11.1.tar.gz", hash = "sha256:5df0ec69510a9e2414c0410d91a698ded5a04d3dd37f7d2a3e119e3c42a30647"}, + {file = "opentelemetry_proto-1.11.1-py3-none-any.whl", hash = "sha256:4d4663123b4777823aa533f478c6cef3ecbcf696d8dc6ac7fd6a90f37a01eafd"}, +] + +[package.dependencies] +protobuf = ">=3.13.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.34.1" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_sdk-1.34.1-py3-none-any.whl", hash = "sha256:308effad4059562f1d92163c61c8141df649da24ce361827812c40abb2a1e96e"}, + {file = "opentelemetry_sdk-1.34.1.tar.gz", hash = "sha256:8091db0d763fcd6098d4781bbc80ff0971f94e260739aa6afe6fd379cdf3aa4d"}, +] + +[package.dependencies] +opentelemetry-api = "1.34.1" +opentelemetry-semantic-conventions = "0.55b1" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.55b1" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_semantic_conventions-0.55b1-py3-none-any.whl", hash = "sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed"}, + {file = "opentelemetry_semantic_conventions-0.55b1.tar.gz", hash = "sha256:ef95b1f009159c28d7a7849f5cbc71c4c34c845bb514d66adfdf1b3fff3598b3"}, +] + +[package.dependencies] +opentelemetry-api = "1.34.1" +typing-extensions = ">=4.5.0" + [[package]] name = "overrides" version = "7.7.0" description = "A decorator to automatically detect mismatch when overriding a method." optional = false python-versions = ">=3.6" +groups = ["test"] files = [ {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, @@ -2369,69 +2585,70 @@ files = [ [[package]] name = "packaging" -version = "24.1" +version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "docs", "test"] files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] name = "pandas" -version = "2.2.3" +version = "2.3.1" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" -files = [ - {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, - {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, - {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, - {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, - {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, - {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, - {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, - {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, - {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, - {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, - {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, - {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, - {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, - {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, - {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, - {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, - {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, - {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, - {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, - {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, - {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, - {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, - {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, - {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, - {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, - {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, - {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, - {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, - {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, - {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, - {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, - {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, - {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, - {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, - {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, - {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, - {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, - {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, - {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, - {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, - {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, - {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, +groups = ["main"] +files = [ + {file = "pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22c2e866f7209ebc3a8f08d75766566aae02bcc91d196935a1d9e59c7b990ac9"}, + {file = "pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3583d348546201aff730c8c47e49bc159833f971c2899d6097bce68b9112a4f1"}, + {file = "pandas-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f951fbb702dacd390561e0ea45cdd8ecfa7fb56935eb3dd78e306c19104b9b0"}, + {file = "pandas-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd05b72ec02ebfb993569b4931b2e16fbb4d6ad6ce80224a3ee838387d83a191"}, + {file = "pandas-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1b916a627919a247d865aed068eb65eb91a344b13f5b57ab9f610b7716c92de1"}, + {file = "pandas-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fe67dc676818c186d5a3d5425250e40f179c2a89145df477dd82945eaea89e97"}, + {file = "pandas-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eb789ae0274672acbd3c575b0598d213345660120a257b47b5dafdc618aec83"}, + {file = "pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b"}, + {file = "pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f"}, + {file = "pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85"}, + {file = "pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d"}, + {file = "pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678"}, + {file = "pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299"}, + {file = "pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab"}, + {file = "pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3"}, + {file = "pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232"}, + {file = "pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e"}, + {file = "pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4"}, + {file = "pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8"}, + {file = "pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679"}, + {file = "pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8"}, + {file = "pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22"}, + {file = "pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a"}, + {file = "pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928"}, + {file = "pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9"}, + {file = "pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12"}, + {file = "pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb"}, + {file = "pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956"}, + {file = "pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a"}, + {file = "pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9"}, + {file = "pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275"}, + {file = "pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab"}, + {file = "pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96"}, + {file = "pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444"}, + {file = "pandas-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4645f770f98d656f11c69e81aeb21c6fca076a44bed3dcbb9396a4311bc7f6d8"}, + {file = "pandas-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:342e59589cc454aaff7484d75b816a433350b3d7964d7847327edda4d532a2e3"}, + {file = "pandas-2.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d12f618d80379fde6af007f65f0c25bd3e40251dbd1636480dfffce2cf1e6da"}, + {file = "pandas-2.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd71c47a911da120d72ef173aeac0bf5241423f9bfea57320110a978457e069e"}, + {file = "pandas-2.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:09e3b1587f0f3b0913e21e8b32c3119174551deb4a4eba4a89bc7377947977e7"}, + {file = "pandas-2.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2323294c73ed50f612f67e2bf3ae45aea04dce5690778e08a09391897f35ff88"}, + {file = "pandas-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4b0de34dc8499c2db34000ef8baad684cfa4cbd836ecee05f323ebfba348c7d"}, + {file = "pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2"}, ] [package.dependencies] numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] @@ -2470,6 +2687,7 @@ version = "1.5.1" description = "Utilities for writing pandoc filters in python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["test"] files = [ {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, @@ -2477,13 +2695,14 @@ files = [ [[package]] name = "paramiko" -version = "3.5.0" +version = "3.5.1" description = "SSH2 protocol library" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "paramiko-3.5.0-py3-none-any.whl", hash = "sha256:1fedf06b085359051cd7d0d270cebe19e755a8a921cc2ddbfa647fb0cd7d68f9"}, - {file = "paramiko-3.5.0.tar.gz", hash = "sha256:ad11e540da4f55cedda52931f1a3f812a8238a7af7f62a60de538cd80bb28124"}, + {file = "paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61"}, + {file = "paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822"}, ] [package.dependencies] @@ -2492,8 +2711,8 @@ cryptography = ">=3.3" pynacl = ">=1.5" [package.extras] -all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] -gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +all = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""] +gssapi = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""] invoke = ["invoke (>=2.0)"] [[package]] @@ -2502,6 +2721,7 @@ version = "0.8.4" description = "A Python Parser" optional = false python-versions = ">=3.6" +groups = ["main", "test"] files = [ {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, @@ -2517,6 +2737,8 @@ version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" +groups = ["main", "test"] +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, @@ -2527,44 +2749,47 @@ ptyprocess = ">=0.5" [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test"] files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev", "test"] files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "prometheus-client" -version = "0.21.0" +version = "0.22.1" description = "Python client for the Prometheus monitoring system." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test"] files = [ - {file = "prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166"}, - {file = "prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e"}, + {file = "prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094"}, + {file = "prometheus_client-0.22.1.tar.gz", hash = "sha256:190f1331e783cf21eb60bca559354e0a4d4378facecf78f5428c39b675d20d28"}, ] [package.extras] @@ -2572,83 +2797,187 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.48" +version = "3.0.51" description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8" +groups = ["main", "test"] files = [ - {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, - {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, + {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, + {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, ] [package.dependencies] wcwidth = "*" +[[package]] +name = "propcache" +version = "0.3.2" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770"}, + {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3"}, + {file = "propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c"}, + {file = "propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70"}, + {file = "propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e"}, + {file = "propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897"}, + {file = "propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1"}, + {file = "propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1"}, + {file = "propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43"}, + {file = "propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02"}, + {file = "propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330"}, + {file = "propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394"}, + {file = "propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe"}, + {file = "propcache-0.3.2-cp39-cp39-win32.whl", hash = "sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1"}, + {file = "propcache-0.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9"}, + {file = "propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f"}, + {file = "propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168"}, +] + [[package]] name = "proto-plus" -version = "1.24.0" -description = "Beautiful, Pythonic protocol buffers." +version = "1.26.1" +description = "Beautiful, Pythonic protocol buffers" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "proto-plus-1.24.0.tar.gz", hash = "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445"}, - {file = "proto_plus-1.24.0-py3-none-any.whl", hash = "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12"}, + {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, + {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, ] [package.dependencies] -protobuf = ">=3.19.0,<6.0.0dev" +protobuf = ">=3.19.0,<7.0.0" [package.extras] testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "5.28.2" +version = "6.31.1" description = "" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "protobuf-5.28.2-cp310-abi3-win32.whl", hash = "sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d"}, - {file = "protobuf-5.28.2-cp310-abi3-win_amd64.whl", hash = "sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132"}, - {file = "protobuf-5.28.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7"}, - {file = "protobuf-5.28.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f"}, - {file = "protobuf-5.28.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f"}, - {file = "protobuf-5.28.2-cp38-cp38-win32.whl", hash = "sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0"}, - {file = "protobuf-5.28.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3"}, - {file = "protobuf-5.28.2-cp39-cp39-win32.whl", hash = "sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36"}, - {file = "protobuf-5.28.2-cp39-cp39-win_amd64.whl", hash = "sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276"}, - {file = "protobuf-5.28.2-py3-none-any.whl", hash = "sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece"}, - {file = "protobuf-5.28.2.tar.gz", hash = "sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0"}, + {file = "protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9"}, + {file = "protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447"}, + {file = "protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402"}, + {file = "protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39"}, + {file = "protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6"}, + {file = "protobuf-6.31.1-cp39-cp39-win32.whl", hash = "sha256:0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16"}, + {file = "protobuf-6.31.1-cp39-cp39-win_amd64.whl", hash = "sha256:8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9"}, + {file = "protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e"}, + {file = "protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a"}, ] [[package]] name = "psutil" -version = "6.0.0" -description = "Cross-platform lib for process and system monitoring in Python." +version = "7.0.0" +description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.6" +groups = ["test"] files = [ - {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, - {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, - {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, - {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, - {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, - {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, - {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, - {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, - {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, - {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, + {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, + {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, + {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, + {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, + {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, + {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, + {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, ] [package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] [[package]] name = "ptyprocess" @@ -2656,10 +2985,12 @@ version = "0.7.0" description = "Run a subprocess in a pseudo terminal" optional = false python-versions = "*" +groups = ["main", "test"] files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] +markers = {main = "sys_platform != \"win32\" and sys_platform != \"emscripten\"", test = "sys_platform != \"win32\" and sys_platform != \"emscripten\" or os_name != \"nt\""} [[package]] name = "pure-eval" @@ -2667,6 +2998,7 @@ version = "0.2.3" description = "Safely evaluate AST nodes without side effects" optional = false python-versions = "*" +groups = ["main", "test"] files = [ {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, @@ -2677,18 +3009,20 @@ tests = ["pytest"] [[package]] name = "py-spy" -version = "0.3.14" +version = "0.4.0" description = "Sampling profiler for Python programs" optional = false python-versions = "*" +groups = ["main"] files = [ - {file = "py_spy-0.3.14-py2.py3-none-macosx_10_7_x86_64.whl", hash = "sha256:5b342cc5feb8d160d57a7ff308de153f6be68dcf506ad02b4d67065f2bae7f45"}, - {file = "py_spy-0.3.14-py2.py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:fe7efe6c91f723442259d428bf1f9ddb9c1679828866b353d539345ca40d9dd2"}, - {file = "py_spy-0.3.14-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590905447241d789d9de36cff9f52067b6f18d8b5e9fb399242041568d414461"}, - {file = "py_spy-0.3.14-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6211fe7f587b3532ba9d300784326d9a6f2b890af7bf6fff21a029ebbc812b"}, - {file = "py_spy-0.3.14-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3e8e48032e71c94c3dd51694c39e762e4bbfec250df5bf514adcdd64e79371e0"}, - {file = "py_spy-0.3.14-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f59b0b52e56ba9566305236375e6fc68888261d0d36b5addbe3cf85affbefc0e"}, - {file = "py_spy-0.3.14-py2.py3-none-win_amd64.whl", hash = "sha256:8f5b311d09f3a8e33dbd0d44fc6e37b715e8e0c7efefafcda8bfd63b31ab5a31"}, + {file = "py_spy-0.4.0-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f2cf3f7130e7d780471faa5957441d3b4e0ec39a79b2c00f4c33d494f7728428"}, + {file = "py_spy-0.4.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:47cdda4c34d9b6cb01f3aaeceb2e88faf57da880207fe72ff6ff97e9bb6cc8a9"}, + {file = "py_spy-0.4.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eee3d0bde85ca5cf4f01f012d461180ca76c24835a96f7b5c4ded64eb6a008ab"}, + {file = "py_spy-0.4.0-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5f06ffce4c9c98b7fc9f5e67e5e7db591173f1351837633f3f23d9378b1d18a"}, + {file = "py_spy-0.4.0-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:87573e64dbfdfc89ba2e0f5e2f525aa84e0299c7eb6454b47ea335fde583a7a0"}, + {file = "py_spy-0.4.0-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8bf2f3702cef367a489faa45177b41a6c31b2a3e5bd78c978d44e29340152f5a"}, + {file = "py_spy-0.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:77d8f637ade38367d944874776f45b703b7ac5938b1f7be8891f3a5876ddbb96"}, + {file = "py_spy-0.4.0.tar.gz", hash = "sha256:806602ce7972782cc9c1e383f339bfc27bfb822d42485e6a3e0530ae5040e1f0"}, ] [[package]] @@ -2697,6 +3031,8 @@ version = "17.0.0" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "sys_platform == \"darwin\" and platform_machine == \"x86_64\"" files = [ {file = "pyarrow-17.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07"}, {file = "pyarrow-17.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655"}, @@ -2742,12 +3078,82 @@ numpy = ">=1.16.6" [package.extras] test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] +[[package]] +name = "pyarrow" +version = "20.0.0" +description = "Python library for Apache Arrow" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "sys_platform != \"darwin\" or platform_machine != \"x86_64\"" +files = [ + {file = "pyarrow-20.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:c7dd06fd7d7b410ca5dc839cc9d485d2bc4ae5240851bcd45d85105cc90a47d7"}, + {file = "pyarrow-20.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d5382de8dc34c943249b01c19110783d0d64b207167c728461add1ecc2db88e4"}, + {file = "pyarrow-20.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6415a0d0174487456ddc9beaead703d0ded5966129fa4fd3114d76b5d1c5ceae"}, + {file = "pyarrow-20.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15aa1b3b2587e74328a730457068dc6c89e6dcbf438d4369f572af9d320a25ee"}, + {file = "pyarrow-20.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5605919fbe67a7948c1f03b9f3727d82846c053cd2ce9303ace791855923fd20"}, + {file = "pyarrow-20.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a5704f29a74b81673d266e5ec1fe376f060627c2e42c5c7651288ed4b0db29e9"}, + {file = "pyarrow-20.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:00138f79ee1b5aca81e2bdedb91e3739b987245e11fa3c826f9e57c5d102fb75"}, + {file = "pyarrow-20.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f2d67ac28f57a362f1a2c1e6fa98bfe2f03230f7e15927aecd067433b1e70ce8"}, + {file = "pyarrow-20.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:4a8b029a07956b8d7bd742ffca25374dd3f634b35e46cc7a7c3fa4c75b297191"}, + {file = "pyarrow-20.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:24ca380585444cb2a31324c546a9a56abbe87e26069189e14bdba19c86c049f0"}, + {file = "pyarrow-20.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:95b330059ddfdc591a3225f2d272123be26c8fa76e8c9ee1a77aad507361cfdb"}, + {file = "pyarrow-20.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f0fb1041267e9968c6d0d2ce3ff92e3928b243e2b6d11eeb84d9ac547308232"}, + {file = "pyarrow-20.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ff87cc837601532cc8242d2f7e09b4e02404de1b797aee747dd4ba4bd6313f"}, + {file = "pyarrow-20.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7a3a5dcf54286e6141d5114522cf31dd67a9e7c9133d150799f30ee302a7a1ab"}, + {file = "pyarrow-20.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a6ad3e7758ecf559900261a4df985662df54fb7fdb55e8e3b3aa99b23d526b62"}, + {file = "pyarrow-20.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6bb830757103a6cb300a04610e08d9636f0cd223d32f388418ea893a3e655f1c"}, + {file = "pyarrow-20.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96e37f0766ecb4514a899d9a3554fadda770fb57ddf42b63d80f14bc20aa7db3"}, + {file = "pyarrow-20.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3346babb516f4b6fd790da99b98bed9708e3f02e734c84971faccb20736848dc"}, + {file = "pyarrow-20.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:75a51a5b0eef32727a247707d4755322cb970be7e935172b6a3a9f9ae98404ba"}, + {file = "pyarrow-20.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:211d5e84cecc640c7a3ab900f930aaff5cd2702177e0d562d426fb7c4f737781"}, + {file = "pyarrow-20.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba3cf4182828be7a896cbd232aa8dd6a31bd1f9e32776cc3796c012855e1199"}, + {file = "pyarrow-20.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c3a01f313ffe27ac4126f4c2e5ea0f36a5fc6ab51f8726cf41fee4b256680bd"}, + {file = "pyarrow-20.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a2791f69ad72addd33510fec7bb14ee06c2a448e06b649e264c094c5b5f7ce28"}, + {file = "pyarrow-20.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4250e28a22302ce8692d3a0e8ec9d9dde54ec00d237cff4dfa9c1fbf79e472a8"}, + {file = "pyarrow-20.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:89e030dc58fc760e4010148e6ff164d2f44441490280ef1e97a542375e41058e"}, + {file = "pyarrow-20.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6102b4864d77102dbbb72965618e204e550135a940c2534711d5ffa787df2a5a"}, + {file = "pyarrow-20.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:96d6a0a37d9c98be08f5ed6a10831d88d52cac7b13f5287f1e0f625a0de8062b"}, + {file = "pyarrow-20.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a15532e77b94c61efadde86d10957950392999503b3616b2ffcef7621a002893"}, + {file = "pyarrow-20.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:dd43f58037443af715f34f1322c782ec463a3c8a94a85fdb2d987ceb5658e061"}, + {file = "pyarrow-20.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa0d288143a8585806e3cc7c39566407aab646fb9ece164609dac1cfff45f6ae"}, + {file = "pyarrow-20.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6953f0114f8d6f3d905d98e987d0924dabce59c3cda380bdfaa25a6201563b4"}, + {file = "pyarrow-20.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:991f85b48a8a5e839b2128590ce07611fae48a904cae6cab1f089c5955b57eb5"}, + {file = "pyarrow-20.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:97c8dc984ed09cb07d618d57d8d4b67a5100a30c3818c2fb0b04599f0da2de7b"}, + {file = "pyarrow-20.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9b71daf534f4745818f96c214dbc1e6124d7daf059167330b610fc69b6f3d3e3"}, + {file = "pyarrow-20.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8b88758f9303fa5a83d6c90e176714b2fd3852e776fc2d7e42a22dd6c2fb368"}, + {file = "pyarrow-20.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:30b3051b7975801c1e1d387e17c588d8ab05ced9b1e14eec57915f79869b5031"}, + {file = "pyarrow-20.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:ca151afa4f9b7bc45bcc791eb9a89e90a9eb2772767d0b1e5389609c7d03db63"}, + {file = "pyarrow-20.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:4680f01ecd86e0dd63e39eb5cd59ef9ff24a9d166db328679e36c108dc993d4c"}, + {file = "pyarrow-20.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4c8534e2ff059765647aa69b75d6543f9fef59e2cd4c6d18015192565d2b70"}, + {file = "pyarrow-20.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e1f8a47f4b4ae4c69c4d702cfbdfe4d41e18e5c7ef6f1bb1c50918c1e81c57b"}, + {file = "pyarrow-20.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:a1f60dc14658efaa927f8214734f6a01a806d7690be4b3232ba526836d216122"}, + {file = "pyarrow-20.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:204a846dca751428991346976b914d6d2a82ae5b8316a6ed99789ebf976551e6"}, + {file = "pyarrow-20.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f3b117b922af5e4c6b9a9115825726cac7d8b1421c37c2b5e24fbacc8930612c"}, + {file = "pyarrow-20.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e724a3fd23ae5b9c010e7be857f4405ed5e679db5c93e66204db1a69f733936a"}, + {file = "pyarrow-20.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:82f1ee5133bd8f49d31be1299dc07f585136679666b502540db854968576faf9"}, + {file = "pyarrow-20.0.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:1bcbe471ef3349be7714261dea28fe280db574f9d0f77eeccc195a2d161fd861"}, + {file = "pyarrow-20.0.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:a18a14baef7d7ae49247e75641fd8bcbb39f44ed49a9fc4ec2f65d5031aa3b96"}, + {file = "pyarrow-20.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb497649e505dc36542d0e68eca1a3c94ecbe9799cb67b578b55f2441a247fbc"}, + {file = "pyarrow-20.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11529a2283cb1f6271d7c23e4a8f9f8b7fd173f7360776b668e509d712a02eec"}, + {file = "pyarrow-20.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fc1499ed3b4b57ee4e090e1cea6eb3584793fe3d1b4297bbf53f09b434991a5"}, + {file = "pyarrow-20.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:db53390eaf8a4dab4dbd6d93c85c5cf002db24902dbff0ca7d988beb5c9dd15b"}, + {file = "pyarrow-20.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:851c6a8260ad387caf82d2bbf54759130534723e37083111d4ed481cb253cc0d"}, + {file = "pyarrow-20.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e22f80b97a271f0a7d9cd07394a7d348f80d3ac63ed7cc38b6d1b696ab3b2619"}, + {file = "pyarrow-20.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:9965a050048ab02409fb7cbbefeedba04d3d67f2cc899eff505cc084345959ca"}, + {file = "pyarrow-20.0.0.tar.gz", hash = "sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1"}, +] + +[package.extras] +test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] + [[package]] name = "pyasn1" version = "0.6.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, @@ -2755,17 +3161,18 @@ files = [ [[package]] name = "pyasn1-modules" -version = "0.4.1" +version = "0.4.2" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, - {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, ] [package.dependencies] -pyasn1 = ">=0.4.6,<0.7.0" +pyasn1 = ">=0.6.1,<0.7.0" [[package]] name = "pycparser" @@ -2773,6 +3180,7 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -2780,72 +3188,148 @@ files = [ [[package]] name = "pydantic" -version = "1.10.18" -description = "Data validation and settings management using python type hints" +version = "2.11.7" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"}, - {file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"}, - {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"}, - {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"}, - {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"}, - {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"}, - {file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"}, - {file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"}, - {file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"}, - {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"}, - {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"}, - {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"}, - {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"}, - {file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"}, - {file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"}, - {file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"}, - {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"}, - {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"}, - {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"}, - {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"}, - {file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"}, - {file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"}, - {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"}, - {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"}, - {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"}, - {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"}, - {file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"}, - {file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"}, - {file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"}, - {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"}, - {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"}, - {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"}, - {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"}, - {file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"}, - {file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"}, - {file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"}, - {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"}, - {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"}, - {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"}, - {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"}, - {file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"}, - {file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"}, - {file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"}, + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs", "test"] files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] @@ -2857,6 +3341,7 @@ version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" optional = false python-versions = ">=3.6" +groups = ["main"] 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"}, @@ -2883,6 +3368,7 @@ version = "7.4.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, @@ -2890,11 +3376,9 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -2905,6 +3389,7 @@ version = "3.11.1" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, @@ -2922,6 +3407,7 @@ version = "2.3.1" description = "pytest plugin to abort hanging tests" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, @@ -2932,76 +3418,94 @@ pytest = ">=7.0.0" [[package]] name = "python-dateutil" -version = "2.9.0.post0" +version = "3.9.0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "test"] files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, + {file = "python-dateutil-3.9.0.tar.gz", hash = "sha256:e090c9a06b858a55d8b6a518fc54d079646eb7262b373ff98f8f13877a5327ec"}, + {file = "python_dateutil-3.9.0-py2.py3-none-any.whl", hash = "sha256:971787138d3cb47d927800e544872edc9e49f33ad1335adc139c409aa5e6a9a8"}, ] [package.dependencies] six = ">=1.5" +[package.source] +type = "legacy" +url = "https://test.pypi.org/simple" +reference = "testpypi" + [[package]] name = "python-json-logger" -version = "2.0.7" -description = "A python library adding a json log formatter" +version = "3.3.0" +description = "JSON Log Formatter for the Python Logging Package" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, - {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, + {file = "python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7"}, + {file = "python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84"}, ] +[package.extras] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec ; implementation_name != \"pypy\"", "mypy", "orjson ; implementation_name != \"pypy\"", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] + [[package]] name = "pytz" -version = "2024.2" +version = "2025.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["main"] files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, ] [[package]] name = "pywin32" -version = "306" +version = "310" description = "Python for Window Extensions" optional = false python-versions = "*" -files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, - {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +groups = ["test"] +markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1"}, + {file = "pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d"}, + {file = "pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213"}, + {file = "pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd"}, + {file = "pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c"}, + {file = "pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582"}, + {file = "pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d"}, + {file = "pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060"}, + {file = "pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966"}, + {file = "pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab"}, + {file = "pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e"}, + {file = "pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33"}, + {file = "pywin32-310-cp38-cp38-win32.whl", hash = "sha256:0867beb8addefa2e3979d4084352e4ac6e991ca45373390775f7084cc0209b9c"}, + {file = "pywin32-310-cp38-cp38-win_amd64.whl", hash = "sha256:30f0a9b3138fb5e07eb4973b7077e1883f558e40c578c6925acc7a94c34eaa36"}, + {file = "pywin32-310-cp39-cp39-win32.whl", hash = "sha256:851c8d927af0d879221e616ae1f66145253537bbdd321a77e8ef701b443a9a1a"}, + {file = "pywin32-310-cp39-cp39-win_amd64.whl", hash = "sha256:96867217335559ac619f00ad70e513c0fcf84b8a3af9fc2bba3b59b97da70475"}, ] [[package]] name = "pywinpty" -version = "2.0.13" +version = "2.0.15" description = "Pseudo terminal support for Windows from Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["test"] +markers = "os_name == \"nt\"" files = [ - {file = "pywinpty-2.0.13-cp310-none-win_amd64.whl", hash = "sha256:697bff211fb5a6508fee2dc6ff174ce03f34a9a233df9d8b5fe9c8ce4d5eaf56"}, - {file = "pywinpty-2.0.13-cp311-none-win_amd64.whl", hash = "sha256:b96fb14698db1284db84ca38c79f15b4cfdc3172065b5137383910567591fa99"}, - {file = "pywinpty-2.0.13-cp312-none-win_amd64.whl", hash = "sha256:2fd876b82ca750bb1333236ce98488c1be96b08f4f7647cfdf4129dfad83c2d4"}, - {file = "pywinpty-2.0.13-cp38-none-win_amd64.whl", hash = "sha256:61d420c2116c0212808d31625611b51caf621fe67f8a6377e2e8b617ea1c1f7d"}, - {file = "pywinpty-2.0.13-cp39-none-win_amd64.whl", hash = "sha256:71cb613a9ee24174730ac7ae439fd179ca34ccb8c5349e8d7b72ab5dea2c6f4b"}, - {file = "pywinpty-2.0.13.tar.gz", hash = "sha256:c34e32351a3313ddd0d7da23d27f835c860d32fe4ac814d372a3ea9594f41dde"}, + {file = "pywinpty-2.0.15-cp310-cp310-win_amd64.whl", hash = "sha256:8e7f5de756a615a38b96cd86fa3cd65f901ce54ce147a3179c45907fa11b4c4e"}, + {file = "pywinpty-2.0.15-cp311-cp311-win_amd64.whl", hash = "sha256:9a6bcec2df2707aaa9d08b86071970ee32c5026e10bcc3cc5f6f391d85baf7ca"}, + {file = "pywinpty-2.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:83a8f20b430bbc5d8957249f875341a60219a4e971580f2ba694fbfb54a45ebc"}, + {file = "pywinpty-2.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:ab5920877dd632c124b4ed17bc6dd6ef3b9f86cd492b963ffdb1a67b85b0f408"}, + {file = "pywinpty-2.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:a4560ad8c01e537708d2790dbe7da7d986791de805d89dd0d3697ca59e9e4901"}, + {file = "pywinpty-2.0.15-cp39-cp39-win_amd64.whl", hash = "sha256:d261cd88fcd358cfb48a7ca0700db3e1c088c9c10403c9ebc0d8a8b57aa6a117"}, + {file = "pywinpty-2.0.15.tar.gz", hash = "sha256:312cf39153a8736c617d45ce8b6ad6cd2107de121df91c455b10ce6bba7a39b2"}, ] [[package]] @@ -3010,6 +3514,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -3068,120 +3573,91 @@ files = [ [[package]] name = "pyzmq" -version = "26.2.0" +version = "27.0.0" description = "Python bindings for 0MQ" optional = false -python-versions = ">=3.7" -files = [ - {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"}, - {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"}, - {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764"}, - {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c"}, - {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a"}, - {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88"}, - {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f"}, - {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282"}, - {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea"}, - {file = "pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2"}, - {file = "pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971"}, - {file = "pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa"}, - {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218"}, - {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4"}, - {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef"}, - {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317"}, - {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf"}, - {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e"}, - {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37"}, - {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3"}, - {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6"}, - {file = "pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4"}, - {file = "pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5"}, - {file = "pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003"}, - {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9"}, - {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52"}, - {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08"}, - {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5"}, - {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae"}, - {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711"}, - {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6"}, - {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3"}, - {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b"}, - {file = "pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7"}, - {file = "pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a"}, - {file = "pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b"}, - {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726"}, - {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3"}, - {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50"}, - {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb"}, - {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187"}, - {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b"}, - {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18"}, - {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115"}, - {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e"}, - {file = "pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5"}, - {file = "pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad"}, - {file = "pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797"}, - {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a"}, - {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc"}, - {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5"}, - {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672"}, - {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797"}, - {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386"}, - {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306"}, - {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6"}, - {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0"}, - {file = "pyzmq-26.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b55a4229ce5da9497dd0452b914556ae58e96a4381bb6f59f1305dfd7e53fc8"}, - {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cb3a6460cdea8fe8194a76de8895707e61ded10ad0be97188cc8463ffa7e3a8"}, - {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ab5cad923cc95c87bffee098a27856c859bd5d0af31bd346035aa816b081fe1"}, - {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ed69074a610fad1c2fda66180e7b2edd4d31c53f2d1872bc2d1211563904cd9"}, - {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cccba051221b916a4f5e538997c45d7d136a5646442b1231b916d0164067ea27"}, - {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0eaa83fc4c1e271c24eaf8fb083cbccef8fde77ec8cd45f3c35a9a123e6da097"}, - {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9edda2df81daa129b25a39b86cb57dfdfe16f7ec15b42b19bfac503360d27a93"}, - {file = "pyzmq-26.2.0-cp37-cp37m-win32.whl", hash = "sha256:ea0eb6af8a17fa272f7b98d7bebfab7836a0d62738e16ba380f440fceca2d951"}, - {file = "pyzmq-26.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4ff9dc6bc1664bb9eec25cd17506ef6672d506115095411e237d571e92a58231"}, - {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f"}, - {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2"}, - {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6"}, - {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289"}, - {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732"}, - {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780"}, - {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640"}, - {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd"}, - {file = "pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988"}, - {file = "pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f"}, - {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2"}, - {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c"}, - {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98"}, - {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9"}, - {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db"}, - {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073"}, - {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc"}, - {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940"}, - {file = "pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44"}, - {file = "pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec"}, - {file = "pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb"}, - {file = "pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072"}, - {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1"}, - {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d"}, - {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca"}, - {file = "pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c"}, - {file = "pyzmq-26.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ea4ad4e6a12e454de05f2949d4beddb52460f3de7c8b9d5c46fbb7d7222e02c"}, - {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fc4f7a173a5609631bb0c42c23d12c49df3966f89f496a51d3eb0ec81f4519d6"}, - {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:878206a45202247781472a2d99df12a176fef806ca175799e1c6ad263510d57c"}, - {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17c412bad2eb9468e876f556eb4ee910e62d721d2c7a53c7fa31e643d35352e6"}, - {file = "pyzmq-26.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0d987a3ae5a71c6226b203cfd298720e0086c7fe7c74f35fa8edddfbd6597eed"}, - {file = "pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20"}, - {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919"}, - {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5"}, - {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc"}, - {file = "pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f"}, - {file = "pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f"}, +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "pyzmq-27.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:b973ee650e8f442ce482c1d99ca7ab537c69098d53a3d046676a484fd710c87a"}, + {file = "pyzmq-27.0.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:661942bc7cd0223d569d808f2e5696d9cc120acc73bf3e88a1f1be7ab648a7e4"}, + {file = "pyzmq-27.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50360fb2a056ffd16e5f4177eee67f1dd1017332ea53fb095fe7b5bf29c70246"}, + {file = "pyzmq-27.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf209a6dc4b420ed32a7093642843cbf8703ed0a7d86c16c0b98af46762ebefb"}, + {file = "pyzmq-27.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c2dace4a7041cca2fba5357a2d7c97c5effdf52f63a1ef252cfa496875a3762d"}, + {file = "pyzmq-27.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:63af72b2955fc77caf0a77444baa2431fcabb4370219da38e1a9f8d12aaebe28"}, + {file = "pyzmq-27.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e8c4adce8e37e75c4215297d7745551b8dcfa5f728f23ce09bf4e678a9399413"}, + {file = "pyzmq-27.0.0-cp310-cp310-win32.whl", hash = "sha256:5d5ef4718ecab24f785794e0e7536436698b459bfbc19a1650ef55280119d93b"}, + {file = "pyzmq-27.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:e40609380480b3d12c30f841323f42451c755b8fece84235236f5fe5ffca8c1c"}, + {file = "pyzmq-27.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6b0397b0be277b46762956f576e04dc06ced265759e8c2ff41a0ee1aa0064198"}, + {file = "pyzmq-27.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:21457825249b2a53834fa969c69713f8b5a79583689387a5e7aed880963ac564"}, + {file = "pyzmq-27.0.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1958947983fef513e6e98eff9cb487b60bf14f588dc0e6bf35fa13751d2c8251"}, + {file = "pyzmq-27.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0dc628b5493f9a8cd9844b8bee9732ef587ab00002157c9329e4fc0ef4d3afa"}, + {file = "pyzmq-27.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7bbe9e1ed2c8d3da736a15694d87c12493e54cc9dc9790796f0321794bbc91f"}, + {file = "pyzmq-27.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc1091f59143b471d19eb64f54bae4f54bcf2a466ffb66fe45d94d8d734eb495"}, + {file = "pyzmq-27.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7011ade88c8e535cf140f8d1a59428676fbbce7c6e54fefce58bf117aefb6667"}, + {file = "pyzmq-27.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c386339d7e3f064213aede5d03d054b237937fbca6dd2197ac8cf3b25a6b14e"}, + {file = "pyzmq-27.0.0-cp311-cp311-win32.whl", hash = "sha256:0546a720c1f407b2172cb04b6b094a78773491497e3644863cf5c96c42df8cff"}, + {file = "pyzmq-27.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:15f39d50bd6c9091c67315ceb878a4f531957b121d2a05ebd077eb35ddc5efed"}, + {file = "pyzmq-27.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c5817641eebb391a2268c27fecd4162448e03538387093cdbd8bf3510c316b38"}, + {file = "pyzmq-27.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cbabc59dcfaac66655c040dfcb8118f133fb5dde185e5fc152628354c1598e52"}, + {file = "pyzmq-27.0.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cb0ac5179cba4b2f94f1aa208fbb77b62c4c9bf24dd446278b8b602cf85fcda3"}, + {file = "pyzmq-27.0.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a48f0228eab6cbf69fde3aa3c03cbe04e50e623ef92ae395fce47ef8a76152"}, + {file = "pyzmq-27.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111db5f395e09f7e775f759d598f43cb815fc58e0147623c4816486e1a39dc22"}, + {file = "pyzmq-27.0.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c8878011653dcdc27cc2c57e04ff96f0471e797f5c19ac3d7813a245bcb24371"}, + {file = "pyzmq-27.0.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0ed2c1f335ba55b5fdc964622254917d6b782311c50e138863eda409fbb3b6d"}, + {file = "pyzmq-27.0.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e918d70862d4cfd4b1c187310015646a14e1f5917922ab45b29f28f345eeb6be"}, + {file = "pyzmq-27.0.0-cp312-abi3-win32.whl", hash = "sha256:88b4e43cab04c3c0f0d55df3b1eef62df2b629a1a369b5289a58f6fa8b07c4f4"}, + {file = "pyzmq-27.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:dce4199bf5f648a902ce37e7b3afa286f305cd2ef7a8b6ec907470ccb6c8b371"}, + {file = "pyzmq-27.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:56e46bbb85d52c1072b3f809cc1ce77251d560bc036d3a312b96db1afe76db2e"}, + {file = "pyzmq-27.0.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c36ad534c0c29b4afa088dc53543c525b23c0797e01b69fef59b1a9c0e38b688"}, + {file = "pyzmq-27.0.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:67855c14173aec36395d7777aaba3cc527b393821f30143fd20b98e1ff31fd38"}, + {file = "pyzmq-27.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8617c7d43cd8ccdb62aebe984bfed77ca8f036e6c3e46dd3dddda64b10f0ab7a"}, + {file = "pyzmq-27.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:67bfbcbd0a04c575e8103a6061d03e393d9f80ffdb9beb3189261e9e9bc5d5e9"}, + {file = "pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5cd11d46d7b7e5958121b3eaf4cd8638eff3a720ec527692132f05a57f14341d"}, + {file = "pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b801c2e40c5aa6072c2f4876de8dccd100af6d9918d4d0d7aa54a1d982fd4f44"}, + {file = "pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20d5cb29e8c5f76a127c75b6e7a77e846bc4b655c373baa098c26a61b7ecd0ef"}, + {file = "pyzmq-27.0.0-cp313-cp313t-win32.whl", hash = "sha256:a20528da85c7ac7a19b7384e8c3f8fa707841fd85afc4ed56eda59d93e3d98ad"}, + {file = "pyzmq-27.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d8229f2efece6a660ee211d74d91dbc2a76b95544d46c74c615e491900dc107f"}, + {file = "pyzmq-27.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:f4162dbbd9c5c84fb930a36f290b08c93e35fce020d768a16fc8891a2f72bab8"}, + {file = "pyzmq-27.0.0-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4e7d0a8d460fba526cc047333bdcbf172a159b8bd6be8c3eb63a416ff9ba1477"}, + {file = "pyzmq-27.0.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:29f44e3c26b9783816ba9ce274110435d8f5b19bbd82f7a6c7612bb1452a3597"}, + {file = "pyzmq-27.0.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e435540fa1da54667f0026cf1e8407fe6d8a11f1010b7f06b0b17214ebfcf5e"}, + {file = "pyzmq-27.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:51f5726de3532b8222e569990c8aa34664faa97038304644679a51d906e60c6e"}, + {file = "pyzmq-27.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:42c7555123679637c99205b1aa9e8f7d90fe29d4c243c719e347d4852545216c"}, + {file = "pyzmq-27.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a979b7cf9e33d86c4949df527a3018767e5f53bc3b02adf14d4d8db1db63ccc0"}, + {file = "pyzmq-27.0.0-cp38-cp38-win32.whl", hash = "sha256:26b72c5ae20bf59061c3570db835edb81d1e0706ff141747055591c4b41193f8"}, + {file = "pyzmq-27.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:55a0155b148fe0428285a30922f7213539aa84329a5ad828bca4bbbc665c70a4"}, + {file = "pyzmq-27.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:100f6e5052ba42b2533011d34a018a5ace34f8cac67cb03cfa37c8bdae0ca617"}, + {file = "pyzmq-27.0.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:bf6c6b061efd00404b9750e2cfbd9507492c8d4b3721ded76cb03786131be2ed"}, + {file = "pyzmq-27.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee05728c0b0b2484a9fc20466fa776fffb65d95f7317a3419985b8c908563861"}, + {file = "pyzmq-27.0.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cdf07fe0a557b131366f80727ec8ccc4b70d89f1e3f920d94a594d598d754f0"}, + {file = "pyzmq-27.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:90252fa2ff3a104219db1f5ced7032a7b5fc82d7c8d2fec2b9a3e6fd4e25576b"}, + {file = "pyzmq-27.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ea6d441c513bf18c578c73c323acf7b4184507fc244762193aa3a871333c9045"}, + {file = "pyzmq-27.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ae2b34bcfaae20c064948a4113bf8709eee89fd08317eb293ae4ebd69b4d9740"}, + {file = "pyzmq-27.0.0-cp39-cp39-win32.whl", hash = "sha256:5b10bd6f008937705cf6e7bf8b6ece5ca055991e3eb130bca8023e20b86aa9a3"}, + {file = "pyzmq-27.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:00387d12a8af4b24883895f7e6b9495dc20a66027b696536edac35cb988c38f3"}, + {file = "pyzmq-27.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:4c19d39c04c29a6619adfeb19e3735c421b3bfee082f320662f52e59c47202ba"}, + {file = "pyzmq-27.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:656c1866505a5735d0660b7da6d7147174bbf59d4975fc2b7f09f43c9bc25745"}, + {file = "pyzmq-27.0.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74175b9e12779382432dd1d1f5960ebe7465d36649b98a06c6b26be24d173fab"}, + {file = "pyzmq-27.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c6de908465697a8708e4d6843a1e884f567962fc61eb1706856545141d0cbb"}, + {file = "pyzmq-27.0.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c644aaacc01d0df5c7072826df45e67301f191c55f68d7b2916d83a9ddc1b551"}, + {file = "pyzmq-27.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:10f70c1d9a446a85013a36871a296007f6fe4232b530aa254baf9da3f8328bc0"}, + {file = "pyzmq-27.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd1dc59763effd1576f8368047c9c31468fce0af89d76b5067641137506792ae"}, + {file = "pyzmq-27.0.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:60e8cc82d968174650c1860d7b716366caab9973787a1c060cf8043130f7d0f7"}, + {file = "pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14fe7aaac86e4e93ea779a821967360c781d7ac5115b3f1a171ced77065a0174"}, + {file = "pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ad0562d4e6abb785be3e4dd68599c41be821b521da38c402bc9ab2a8e7ebc7e"}, + {file = "pyzmq-27.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9df43a2459cd3a3563404c1456b2c4c69564daa7dbaf15724c09821a3329ce46"}, + {file = "pyzmq-27.0.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c86ea8fe85e2eb0ffa00b53192c401477d5252f6dd1db2e2ed21c1c30d17e5e"}, + {file = "pyzmq-27.0.0-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:c45fee3968834cd291a13da5fac128b696c9592a9493a0f7ce0b47fa03cc574d"}, + {file = "pyzmq-27.0.0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cae73bb6898c4e045fbed5024cb587e4110fddb66f6163bcab5f81f9d4b9c496"}, + {file = "pyzmq-27.0.0-pp38-pypy38_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26d542258c7a1f35a9cff3d887687d3235006134b0ac1c62a6fe1ad3ac10440e"}, + {file = "pyzmq-27.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:04cd50ef3b28e35ced65740fb9956a5b3f77a6ff32fcd887e3210433f437dd0f"}, + {file = "pyzmq-27.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:39ddd3ba0a641f01d8f13a3cfd4c4924eb58e660d8afe87e9061d6e8ca6f7ac3"}, + {file = "pyzmq-27.0.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8ca7e6a0388dd9e1180b14728051068f4efe83e0d2de058b5ff92c63f399a73f"}, + {file = "pyzmq-27.0.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2524c40891be6a3106885a3935d58452dd83eb7a5742a33cc780a1ad4c49dec0"}, + {file = "pyzmq-27.0.0-pp39-pypy39_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a56e3e5bd2d62a01744fd2f1ce21d760c7c65f030e9522738d75932a14ab62a"}, + {file = "pyzmq-27.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:096af9e133fec3a72108ddefba1e42985cb3639e9de52cfd336b6fc23aa083e9"}, + {file = "pyzmq-27.0.0.tar.gz", hash = "sha256:b1f08eeb9ce1510e6939b6e5dcd46a17765e2333daae78ecf4606808442e52cf"}, ] [package.dependencies] @@ -3189,57 +3665,65 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""} [[package]] name = "ray" -version = "2.35.0" +version = "2.47.1" description = "Ray provides a simple, universal API for building distributed applications." optional = false -python-versions = ">=3.8" -files = [ - {file = "ray-2.35.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1e7e2d2e987be728a81821b6fd2bccb23e4d8a6cca8417db08b24f06a08d8476"}, - {file = "ray-2.35.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bd48be4c362004d31e5df072fd58b929efc67adfefc0adece41483b15f84539"}, - {file = "ray-2.35.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:ef41e9254f3e18a90a8cf13fac9e35ac086eb778079ab6c76a37d3a6059186c5"}, - {file = "ray-2.35.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:1994aaf9996ffc45019856545e817d527ad572762f1af76ad669ae4e786fcfd6"}, - {file = "ray-2.35.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3b7a7d73f818e249064460ffa95402ebd852bf97d9ec6167b8b0d95be03da9f"}, - {file = "ray-2.35.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:e29754fac4b69a9cb0d089841af59ec6fb10b5d4a248b7c579d319ca2ed1c96f"}, - {file = "ray-2.35.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7a606c8ca53c64fc496703e9fd15d1a1ffb50e6b457a33d3622be2f13fc30a5"}, - {file = "ray-2.35.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:ac561e20a62ce941b74d02a0b92b7765c6ba87cc22e24f34f64ded2c454ba64e"}, - {file = "ray-2.35.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:587af570cbe5f6cedca854f15107740e63c67207bee900713cb2ee38f6ebf20f"}, - {file = "ray-2.35.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e406cce41679790146d4d2b1b0cb0b413ca35276e43b68ee796366169c1dbde"}, - {file = "ray-2.35.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:eb86355a3a0e794e2f1dbd5a84805dddfca64921ad0999b7fa5276e40d243692"}, - {file = "ray-2.35.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b746913268d5ea5e19bff0eb6bdc7e0538036892a8b57c08411787481195df2"}, - {file = "ray-2.35.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:e2ccfd144180f03d38b02a81afdac2b437f27e46736bf2653a1f0e8d67ea56cd"}, - {file = "ray-2.35.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:2ca1a0de41d4462fd764598a5981cf55fc955599f38f9a1ae10868e94c6dd80d"}, - {file = "ray-2.35.0-cp312-cp312-win_amd64.whl", hash = "sha256:c5600f745bb0e4df840a5cd51e82b1acf517f73505df9869fe3e369966956129"}, - {file = "ray-2.35.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5e98d2bac394b806109782f316740c5b3c3f10a50117c8e28200a528df734928"}, - {file = "ray-2.35.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c395b46efd0dd871424b1b8d6baf99f91983946fbe351ff66ea34e8919daff29"}, - {file = "ray-2.35.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4e6314bfdb8c73abcac13f41cc3d935dd1a8ad94c65005a4bfdc4861dc8b070d"}, - {file = "ray-2.35.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:70a154e3071cbb4d7a9b68f2dcf491b96b760be0ec6e2ef11a766071ac6acfef"}, - {file = "ray-2.35.0-cp39-cp39-win_amd64.whl", hash = "sha256:dd8bdf9d16989684486db9ebcd23679140e2d6769fcdaadc05e8cac6b373023e"}, +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "ray-2.47.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36a30930e8d265e708df96f37f6f1f5484f4b97090d505912f992e045a69d310"}, + {file = "ray-2.47.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7c03a1e366d3a868a55f8c2f728f5ce35ac85ddf093ac81d0c1a35bf1c25c377"}, + {file = "ray-2.47.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:6fc7df8657b8df684b77c2d1b643137ad745aa1c12ade34743f06cca79003df0"}, + {file = "ray-2.47.1-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:84a96b4720175a0000521a48eb7aa915f3b419bb5cd6172d8dee005c3f23b813"}, + {file = "ray-2.47.1-cp310-cp310-win_amd64.whl", hash = "sha256:44900a1a72cb3bfb331db160a8975737c25945a97f376c70e72ccf35adf3b744"}, + {file = "ray-2.47.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a640d447e0e6cf63f85b9220c883ec02bb2b8e40a9c1d84efa012795c769ba68"}, + {file = "ray-2.47.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:feeba1e715cfd8737d3adcd2018d0cdabb7c6084fa4b093e638e6c7d42f3c956"}, + {file = "ray-2.47.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:db5ff652e9035f03c65e1742a706b76519f6e8a6744cc005396053ac8766fc46"}, + {file = "ray-2.47.1-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:48961229614b2b56a535be510c8abc76e99a9aa7fa195b5c949bd0c6c69af40a"}, + {file = "ray-2.47.1-cp311-cp311-win_amd64.whl", hash = "sha256:bd1cba64070db06bbf79c0e075cdc4529193e2d0b19564f4f057b4193b29e912"}, + {file = "ray-2.47.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:322049c4546cf67e5efdad90c371c5508acbb193e5aaaf4038103c6c5ce1f578"}, + {file = "ray-2.47.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:e6d9c78e53ac89cabbc4056aecfec53c506c692e3132af9dae941d6180ef462f"}, + {file = "ray-2.47.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:cd4e7eb475487364b5209963b17cefedcb7fbd3a816fdb6def7ea533ebd72424"}, + {file = "ray-2.47.1-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:3eaeaeec3bbe2ca6493e530c30473d84b8580a7ac3256bb9183d8c63def5a92f"}, + {file = "ray-2.47.1-cp312-cp312-win_amd64.whl", hash = "sha256:601f23ba89918b7b3ffebf967328f7bdb605deaf8c103aad7820dc2722fe450c"}, + {file = "ray-2.47.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cd625d469ce15391e5f1f44ddf8dd30b2380f917603fa0172661229acb0011f"}, + {file = "ray-2.47.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:e578929f58b3f0c59c7544a96d864e26278238b755d13cd19ae798070c848e57"}, + {file = "ray-2.47.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:d6ed6d182e25d6f77179dc77bc97a749c81765b13cb671a46db3203029389663"}, + {file = "ray-2.47.1-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:252a471e8afb918b105cdbffb4cbebb0143baad75a06c8ffcde27ac317579ccb"}, + {file = "ray-2.47.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c21720f283a3df360ddec002a592ddfbaf520faf4cb1b86562a7b7c196ad96a0"}, + {file = "ray-2.47.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:6c7b4abe112c4d698243e30023bcbffe2c2c9a68416b95a6a0d50f9ca5725545"}, + {file = "ray-2.47.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:21f2689c1bbc688f9cd31a18bae2c9582027e91b508073849441167bb5077816"}, + {file = "ray-2.47.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:106817f80087d21d24e63f6e56ea5ab7c387a25105eb65e6b783551f569534ea"}, + {file = "ray-2.47.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee02ba9b8cd45c4eadc457183f6d80f1701b85f966d02cdacd5b11867cb7375"}, ] [package.dependencies] aiohttp = {version = ">=3.7", optional = true, markers = "extra == \"default\""} aiohttp-cors = {version = "*", optional = true, markers = "extra == \"default\""} -aiosignal = "*" click = ">=7.0" colorful = {version = "*", optional = true, markers = "extra == \"default\""} filelock = "*" -frozenlist = "*" fsspec = {version = "*", optional = true, markers = "extra == \"data\""} -grpcio = [ - {version = ">=1.32.0", optional = true, markers = "python_version < \"3.10\" and extra == \"default\""}, - {version = ">=1.42.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"default\""}, -] +grpcio = {version = ">=1.42.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"default\""} jsonschema = "*" -memray = {version = "*", optional = true, markers = "sys_platform != \"win32\" and extra == \"default\""} msgpack = ">=1.0.0,<2.0.0" numpy = {version = ">=1.20", optional = true, markers = "extra == \"data\""} opencensus = {version = "*", optional = true, markers = "extra == \"default\""} +opentelemetry-exporter-prometheus = {version = "*", optional = true, markers = "extra == \"default\""} +opentelemetry-proto = {version = "*", optional = true, markers = "extra == \"default\""} +opentelemetry-sdk = {version = "*", optional = true, markers = "extra == \"default\""} packaging = "*" pandas = {version = ">=1.3", optional = true, markers = "extra == \"data\""} prometheus-client = {version = ">=0.7.1", optional = true, markers = "extra == \"default\""} protobuf = ">=3.15.3,<3.19.5 || >3.19.5" -py-spy = {version = ">=0.2.0", optional = true, markers = "extra == \"default\""} -pyarrow = {version = ">=6.0.1", optional = true, markers = "extra == \"data\""} +py-spy = [ + {version = ">=0.2.0", optional = true, markers = "python_version < \"3.12\" and extra == \"default\""}, + {version = ">=0.4.0", optional = true, markers = "python_version >= \"3.12\" and extra == \"default\""}, +] +pyarrow = [ + {version = ">=9.0.0,<18", optional = true, markers = "sys_platform == \"darwin\" and platform_machine == \"x86_64\" and extra == \"data\""}, + {version = ">=9.0.0", optional = true, markers = "(sys_platform != \"darwin\" or platform_machine != \"x86_64\") and extra == \"data\""}, +] pydantic = {version = "<2.0.dev0 || >=2.5.dev0,<3", optional = true, markers = "extra == \"default\""} pyyaml = "*" requests = "*" @@ -3247,50 +3731,55 @@ smart-open = {version = "*", optional = true, markers = "extra == \"default\""} virtualenv = {version = ">=20.0.24,<20.21.1 || >20.21.1", optional = true, markers = "extra == \"default\""} [package.extras] -adag = ["cupy-cuda12x"] -air = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "fastapi", "fsspec", "grpcio (>=1.32.0)", "grpcio (>=1.42.0)", "memray", "numpy (>=1.20)", "opencensus", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0)", "pyarrow (>=6.0.1)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "starlette", "tensorboardX (>=1.9)", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] -all = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "cupy-cuda12x", "dm-tree", "fastapi", "fsspec", "grpcio (!=1.56.0)", "grpcio (>=1.32.0)", "grpcio (>=1.42.0)", "gymnasium (==0.28.1)", "lz4", "memray", "numpy (>=1.20)", "opencensus", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0)", "pyarrow (>=6.0.1)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "pyyaml", "requests", "rich", "scikit-image", "scipy", "smart-open", "starlette", "tensorboardX (>=1.9)", "typer", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] -all-cpp = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "cupy-cuda12x", "dm-tree", "fastapi", "fsspec", "grpcio (!=1.56.0)", "grpcio (>=1.32.0)", "grpcio (>=1.42.0)", "gymnasium (==0.28.1)", "lz4", "memray", "numpy (>=1.20)", "opencensus", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0)", "pyarrow (>=6.0.1)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "pyyaml", "ray-cpp (==2.35.0)", "requests", "rich", "scikit-image", "scipy", "smart-open", "starlette", "tensorboardX (>=1.9)", "typer", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] -client = ["grpcio (!=1.56.0)"] -cpp = ["ray-cpp (==2.35.0)"] -data = ["fsspec", "numpy (>=1.20)", "pandas (>=1.3)", "pyarrow (>=6.0.1)"] -default = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "grpcio (>=1.32.0)", "grpcio (>=1.42.0)", "memray", "opencensus", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "virtualenv (>=20.0.24,!=20.21.1)"] -observability = ["opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk"] -rllib = ["dm-tree", "fsspec", "gymnasium (==0.28.1)", "lz4", "pandas", "pyarrow (>=6.0.1)", "pyyaml", "requests", "rich", "scikit-image", "scipy", "tensorboardX (>=1.9)", "typer"] -serve = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "fastapi", "grpcio (>=1.32.0)", "grpcio (>=1.42.0)", "memray", "opencensus", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "starlette", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] -serve-grpc = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "fastapi", "grpcio (>=1.32.0)", "grpcio (>=1.42.0)", "memray", "opencensus", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "starlette", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] -train = ["fsspec", "pandas", "pyarrow (>=6.0.1)", "requests", "tensorboardX (>=1.9)"] -tune = ["fsspec", "pandas", "pyarrow (>=6.0.1)", "requests", "tensorboardX (>=1.9)"] +adag = ["cupy-cuda12x ; sys_platform != \"darwin\""] +air = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "fastapi", "fsspec", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "numpy (>=1.20)", "opencensus", "opentelemetry-exporter-prometheus", "opentelemetry-proto", "opentelemetry-sdk", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "starlette", "tensorboardX (>=1.9)", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] +all = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "cupy-cuda12x ; sys_platform != \"darwin\"", "dm-tree", "fastapi", "fsspec", "grpcio", "grpcio (!=1.56.0) ; sys_platform == \"darwin\"", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "gymnasium (==1.0.0)", "lz4", "memray ; sys_platform != \"win32\"", "numpy (>=1.20)", "opencensus", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-exporter-prometheus", "opentelemetry-proto", "opentelemetry-sdk", "ormsgpack (==1.7.0)", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pyOpenSSL", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "pyyaml", "requests", "scipy", "smart-open", "starlette", "tensorboardX (>=1.9)", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] +all-cpp = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "cupy-cuda12x ; sys_platform != \"darwin\"", "dm-tree", "fastapi", "fsspec", "grpcio", "grpcio (!=1.56.0) ; sys_platform == \"darwin\"", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "gymnasium (==1.0.0)", "lz4", "memray ; sys_platform != \"win32\"", "numpy (>=1.20)", "opencensus", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-exporter-prometheus", "opentelemetry-proto", "opentelemetry-sdk", "ormsgpack (==1.7.0)", "pandas", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pyOpenSSL", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "pyyaml", "ray-cpp (==2.47.1)", "requests", "scipy", "smart-open", "starlette", "tensorboardX (>=1.9)", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] +cgraph = ["cupy-cuda12x ; sys_platform != \"darwin\""] +client = ["grpcio", "grpcio (!=1.56.0) ; sys_platform == \"darwin\""] +cpp = ["ray-cpp (==2.47.1)"] +data = ["fsspec", "numpy (>=1.20)", "pandas (>=1.3)", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)"] +default = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "opencensus", "opentelemetry-exporter-prometheus", "opentelemetry-proto", "opentelemetry-sdk", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "virtualenv (>=20.0.24,!=20.21.1)"] +llm = ["aiohttp (>=3.7)", "aiohttp-cors", "async-timeout ; python_version < \"3.11\"", "colorful", "fastapi", "fsspec", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "jsonref (>=1.1.0)", "jsonschema", "ninja", "numpy (>=1.20)", "opencensus", "opentelemetry-exporter-prometheus", "opentelemetry-proto", "opentelemetry-sdk", "pandas (>=1.3)", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "starlette", "typer", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "vllm (>=0.8.5)", "watchfiles"] +observability = ["memray ; sys_platform != \"win32\"", "opentelemetry-api", "opentelemetry-exporter-otlp", "opentelemetry-sdk"] +rllib = ["dm-tree", "fsspec", "gymnasium (==1.0.0)", "lz4", "ormsgpack (==1.7.0)", "pandas", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pyyaml", "requests", "scipy", "tensorboardX (>=1.9)"] +serve = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "fastapi", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "opencensus", "opentelemetry-exporter-prometheus", "opentelemetry-proto", "opentelemetry-sdk", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "starlette", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] +serve-grpc = ["aiohttp (>=3.7)", "aiohttp-cors", "colorful", "fastapi", "grpcio (>=1.32.0) ; python_version < \"3.10\"", "grpcio (>=1.42.0) ; python_version >= \"3.10\"", "opencensus", "opentelemetry-exporter-prometheus", "opentelemetry-proto", "opentelemetry-sdk", "prometheus-client (>=0.7.1)", "py-spy (>=0.2.0) ; python_version < \"3.12\"", "py-spy (>=0.4.0) ; python_version >= \"3.12\"", "pyOpenSSL", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "smart-open", "starlette", "uvicorn[standard]", "virtualenv (>=20.0.24,!=20.21.1)", "watchfiles"] +train = ["fsspec", "pandas", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "pydantic (<2.0.dev0 || >=2.5.dev0,<3)", "requests", "tensorboardX (>=1.9)"] +tune = ["fsspec", "pandas", "pyarrow (<18) ; sys_platform == \"darwin\" and platform_machine == \"x86_64\"", "pyarrow (>=9.0.0)", "requests", "tensorboardX (>=1.9)"] [[package]] name = "referencing" -version = "0.35.1" +version = "0.36.2" description = "JSON Referencing + Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test"] files = [ - {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, - {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, + {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, + {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, ] [package.dependencies] attrs = ">=22.2.0" rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "docs", "test"] files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" @@ -3304,6 +3793,7 @@ version = "2.0.0" description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=3.4" +groups = ["main"] files = [ {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, @@ -3322,6 +3812,7 @@ version = "0.1.4" description = "A pure python RFC3339 validator" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["test"] files = [ {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, @@ -3336,6 +3827,7 @@ version = "0.1.1" description = "Pure python rfc3986 validator" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["test"] files = [ {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, @@ -3347,6 +3839,7 @@ version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, @@ -3355,132 +3848,174 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.20.0" +version = "0.26.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false -python-versions = ">=3.8" -files = [ - {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, - {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, - {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, - {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, - {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, - {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, - {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, - {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, - {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, - {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, - {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, - {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, - {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, - {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, - {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, - {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, - {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, - {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, - {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, - {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, - {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, - {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, - {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, - {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, - {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, +python-versions = ">=3.9" +groups = ["main", "test"] +files = [ + {file = "rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37"}, + {file = "rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0"}, + {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd"}, + {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79"}, + {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3"}, + {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf"}, + {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc"}, + {file = "rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19"}, + {file = "rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11"}, + {file = "rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f"}, + {file = "rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323"}, + {file = "rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45"}, + {file = "rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84"}, + {file = "rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed"}, + {file = "rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0"}, + {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1"}, + {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7"}, + {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6"}, + {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e"}, + {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d"}, + {file = "rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3"}, + {file = "rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107"}, + {file = "rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a"}, + {file = "rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318"}, + {file = "rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a"}, + {file = "rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03"}, + {file = "rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41"}, + {file = "rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d"}, + {file = "rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136"}, + {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582"}, + {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e"}, + {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15"}, + {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8"}, + {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a"}, + {file = "rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323"}, + {file = "rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158"}, + {file = "rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3"}, + {file = "rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2"}, + {file = "rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44"}, + {file = "rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c"}, + {file = "rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8"}, + {file = "rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d"}, + {file = "rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1"}, + {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e"}, + {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1"}, + {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9"}, + {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7"}, + {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04"}, + {file = "rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1"}, + {file = "rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9"}, + {file = "rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9"}, + {file = "rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba"}, + {file = "rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b"}, + {file = "rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5"}, + {file = "rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256"}, + {file = "rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618"}, + {file = "rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35"}, + {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f"}, + {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83"}, + {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1"}, + {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8"}, + {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f"}, + {file = "rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed"}, + {file = "rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632"}, + {file = "rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c"}, + {file = "rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0"}, + {file = "rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9"}, + {file = "rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9"}, + {file = "rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a"}, + {file = "rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf"}, + {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12"}, + {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20"}, + {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331"}, + {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f"}, + {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246"}, + {file = "rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387"}, + {file = "rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af"}, + {file = "rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33"}, + {file = "rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953"}, + {file = "rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9"}, + {file = "rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37"}, + {file = "rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867"}, + {file = "rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da"}, + {file = "rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7"}, + {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad"}, + {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d"}, + {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca"}, + {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19"}, + {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8"}, + {file = "rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b"}, + {file = "rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a"}, + {file = "rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170"}, + {file = "rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e"}, + {file = "rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f"}, + {file = "rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7"}, + {file = "rpds_py-0.26.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7a48af25d9b3c15684059d0d1fc0bc30e8eee5ca521030e2bffddcab5be40226"}, + {file = "rpds_py-0.26.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c71c2f6bf36e61ee5c47b2b9b5d47e4d1baad6426bfed9eea3e858fc6ee8806"}, + {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d815d48b1804ed7867b539236b6dd62997850ca1c91cad187f2ddb1b7bbef19"}, + {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84cfbd4d4d2cdeb2be61a057a258d26b22877266dd905809e94172dff01a42ae"}, + {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbaa70553ca116c77717f513e08815aec458e6b69a028d4028d403b3bc84ff37"}, + {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39bfea47c375f379d8e87ab4bb9eb2c836e4f2069f0f65731d85e55d74666387"}, + {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1533b7eb683fb5f38c1d68a3c78f5fdd8f1412fa6b9bf03b40f450785a0ab915"}, + {file = "rpds_py-0.26.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c5ab0ee51f560d179b057555b4f601b7df909ed31312d301b99f8b9fc6028284"}, + {file = "rpds_py-0.26.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e5162afc9e0d1f9cae3b577d9c29ddbab3505ab39012cb794d94a005825bde21"}, + {file = "rpds_py-0.26.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:43f10b007033f359bc3fa9cd5e6c1e76723f056ffa9a6b5c117cc35720a80292"}, + {file = "rpds_py-0.26.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e3730a48e5622e598293eee0762b09cff34dd3f271530f47b0894891281f051d"}, + {file = "rpds_py-0.26.0-cp39-cp39-win32.whl", hash = "sha256:4b1f66eb81eab2e0ff5775a3a312e5e2e16bf758f7b06be82fb0d04078c7ac51"}, + {file = "rpds_py-0.26.0-cp39-cp39-win_amd64.whl", hash = "sha256:519067e29f67b5c90e64fb1a6b6e9d2ec0ba28705c51956637bac23a2f4ddae1"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b"}, + {file = "rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8"}, + {file = "rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674"}, + {file = "rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696"}, + {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb"}, + {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88"}, + {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8"}, + {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5"}, + {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7"}, + {file = "rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b"}, + {file = "rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb"}, + {file = "rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0"}, + {file = "rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a90a13408a7a856b87be8a9f008fff53c5080eea4e4180f6c2e546e4a972fb5d"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ac51b65e8dc76cf4949419c54c5528adb24fc721df722fd452e5fbc236f5c40"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59b2093224a18c6508d95cfdeba8db9cbfd6f3494e94793b58972933fcee4c6d"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f01a5d6444a3258b00dc07b6ea4733e26f8072b788bef750baa37b370266137"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6e2c12160c72aeda9d1283e612f68804621f448145a210f1bf1d79151c47090"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb28c1f569f8d33b2b5dcd05d0e6ef7005d8639c54c2f0be824f05aedf715255"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1766b5724c3f779317d5321664a343c07773c8c5fd1532e4039e6cc7d1a815be"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b6d9e5a2ed9c4988c8f9b28b3bc0e3e5b1aaa10c28d210a594ff3a8c02742daf"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b5f7a446ddaf6ca0fad9a5535b56fbfc29998bf0e0b450d174bbec0d600e1d72"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:eed5ac260dd545fbc20da5f4f15e7efe36a55e0e7cf706e4ec005b491a9546a0"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:582462833ba7cee52e968b0341b85e392ae53d44c0f9af6a5927c80e539a8b67"}, + {file = "rpds_py-0.26.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69a607203441e07e9a8a529cff1d5b73f6a160f22db1097211e6212a68567d11"}, + {file = "rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0"}, ] [[package]] name = "rsa" -version = "4.9" +version = "4.9.1" description = "Pure-Python RSA implementation" optional = false -python-versions = ">=3.6,<4" +python-versions = "<4,>=3.6" +groups = ["main"] files = [ - {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, - {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, + {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, + {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, ] [package.dependencies] @@ -3492,69 +4027,73 @@ version = "1.8.3" description = "Send file to trash natively under Mac OS X, Windows and Linux" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["test"] files = [ {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, ] [package.extras] -nativelib = ["pyobjc-framework-Cocoa", "pywin32"] -objc = ["pyobjc-framework-Cocoa"] -win32 = ["pywin32"] +nativelib = ["pyobjc-framework-Cocoa ; sys_platform == \"darwin\"", "pywin32 ; sys_platform == \"win32\""] +objc = ["pyobjc-framework-Cocoa ; sys_platform == \"darwin\""] +win32 = ["pywin32 ; sys_platform == \"win32\""] [[package]] name = "setuptools" -version = "75.1.0" +version = "80.9.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["test"] files = [ - {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"}, - {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"}, + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "test"] files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] name = "smart-open" -version = "7.0.4" -description = "Utils for streaming large files (S3, HDFS, GCS, Azure Blob Storage, gzip, bz2...)" +version = "7.3.0.post1" +description = "Utils for streaming large files (S3, HDFS, GCS, SFTP, Azure Blob Storage, gzip, bz2, zst...)" optional = false -python-versions = "<4.0,>=3.7" +python-versions = "<4.0,>=3.8" +groups = ["main"] files = [ - {file = "smart_open-7.0.4-py3-none-any.whl", hash = "sha256:4e98489932b3372595cddc075e6033194775165702887216b65eba760dfd8d47"}, - {file = "smart_open-7.0.4.tar.gz", hash = "sha256:62b65852bdd1d1d516839fcb1f6bc50cd0f16e05b4ec44b52f43d38bcb838524"}, + {file = "smart_open-7.3.0.post1-py3-none-any.whl", hash = "sha256:c73661a2c24bf045c1e04e08fffc585b59af023fe783d57896f590489db66fb4"}, + {file = "smart_open-7.3.0.post1.tar.gz", hash = "sha256:ce6a3d9bc1afbf6234ad13c010b77f8cd36d24636811e3c52c3b5160f5214d1e"}, ] [package.dependencies] wrapt = "*" [package.extras] -all = ["azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "paramiko", "requests", "zstandard"] +all = ["smart_open[azure,gcs,http,s3,ssh,webhdfs,zst]"] azure = ["azure-common", "azure-core", "azure-storage-blob"] gcs = ["google-cloud-storage (>=2.6.0)"] http = ["requests"] s3 = ["boto3"] ssh = ["paramiko"] -test = ["azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "moto[server]", "paramiko", "pytest", "pytest-rerunfailures", "requests", "responses", "zstandard"] +test = ["awscli", "moto[server]", "numpy", "pyopenssl", "pytest", "pytest-rerunfailures", "pytest_benchmark", "responses", "smart_open[all]"] webhdfs = ["requests"] zst = ["zstandard"] @@ -3564,6 +4103,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -3571,24 +4111,26 @@ files = [ [[package]] name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +version = "3.0.1" +description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." optional = false -python-versions = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" +groups = ["docs"] files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, + {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, + {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, ] [[package]] name = "soupsieve" -version = "2.6" +version = "2.7" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, - {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, + {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, + {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, ] [[package]] @@ -3597,6 +4139,7 @@ version = "7.4.7" description = "Python documentation generator" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, @@ -3608,7 +4151,6 @@ babel = ">=2.13" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} docutils = ">=0.20,<0.22" imagesize = ">=1.3" -importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} Jinja2 = ">=3.1" packaging = ">=23.0" Pygments = ">=2.17" @@ -3620,7 +4162,6 @@ sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.9" -tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] @@ -3633,6 +4174,7 @@ version = "3.0.1" description = "Read the Docs theme for Sphinx" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "sphinx_rtd_theme-3.0.1-py2.py3-none-any.whl", hash = "sha256:921c0ece75e90633ee876bd7b148cfaad136b481907ad154ac3669b6fc957916"}, {file = "sphinx_rtd_theme-3.0.1.tar.gz", hash = "sha256:a4c5745d1b06dfcb80b7704fe532eb765b44065a8fad9851e4258c8804140703"}, @@ -3652,6 +4194,7 @@ version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, @@ -3668,6 +4211,7 @@ version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, @@ -3684,6 +4228,7 @@ version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, @@ -3700,6 +4245,7 @@ version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" optional = false python-versions = ">=2.7" +groups = ["docs"] files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, @@ -3714,6 +4260,7 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["docs"] files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -3728,6 +4275,7 @@ version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, @@ -3744,6 +4292,7 @@ version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, @@ -3760,6 +4309,7 @@ version = "0.6.3" description = "Extract data from python stack frames and tracebacks for informative displays" optional = false python-versions = "*" +groups = ["main", "test"] files = [ {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, @@ -3779,6 +4329,7 @@ version = "0.18.1" description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0"}, {file = "terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e"}, @@ -3796,13 +4347,14 @@ typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] [[package]] name = "tinycss2" -version = "1.3.0" +version = "1.4.0" description = "A tiny CSS parser" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"}, - {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"}, + {file = "tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289"}, + {file = "tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7"}, ] [package.dependencies] @@ -3812,35 +4364,26 @@ webencodings = ">=0.4" doc = ["sphinx", "sphinx_rtd_theme"] test = ["pytest", "ruff"] -[[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 = "tornado" -version = "6.4.1" +version = "6.5.1" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["test"] files = [ - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, - {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, - {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, - {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, + {file = "tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7"}, + {file = "tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6"}, + {file = "tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888"}, + {file = "tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331"}, + {file = "tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e"}, + {file = "tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401"}, + {file = "tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692"}, + {file = "tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a"}, + {file = "tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365"}, + {file = "tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b"}, + {file = "tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7"}, + {file = "tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c"}, ] [[package]] @@ -3849,6 +4392,7 @@ version = "5.14.3" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, @@ -3860,35 +4404,53 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, [[package]] name = "types-python-dateutil" -version = "2.9.0.20240906" +version = "2.9.0.20250708" description = "Typing stubs for python-dateutil" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["test"] files = [ - {file = "types-python-dateutil-2.9.0.20240906.tar.gz", hash = "sha256:9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e"}, - {file = "types_python_dateutil-2.9.0.20240906-py3-none-any.whl", hash = "sha256:27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6"}, + {file = "types_python_dateutil-2.9.0.20250708-py3-none-any.whl", hash = "sha256:4d6d0cc1cc4d24a2dc3816024e502564094497b713f7befda4d5bc7a8e3fd21f"}, + {file = "types_python_dateutil-2.9.0.20250708.tar.gz", hash = "sha256:ccdbd75dab2d6c9696c350579f34cffe2c281e4c5f27a585b2a2438dd1d5c8ab"}, ] [[package]] name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "tzdata" -version = "2024.2" +version = "2025.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main"] files = [ - {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, - {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] [[package]] @@ -3897,6 +4459,7 @@ version = "1.3.0" description = "RFC 6570 URI Template Processor" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, @@ -3907,30 +4470,32 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake [[package]] name = "urllib3" -version = "2.2.3" +version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "docs", "test"] files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.6" +version = "20.35.1" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, - {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, + {file = "virtualenv-20.35.1-py3-none-any.whl", hash = "sha256:1d9d93cd01d35b785476e2fa7af711a98d40d227a078941695bbae394f8737e2"}, + {file = "virtualenv-20.35.1.tar.gz", hash = "sha256:041dac43b6899858a91838b616599e80000e545dee01a21172a6a46746472cb2"}, ] [package.dependencies] @@ -3940,7 +4505,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "wcwidth" @@ -3948,6 +4513,7 @@ version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" +groups = ["main", "test"] files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -3955,25 +4521,23 @@ files = [ [[package]] name = "webcolors" -version = "24.8.0" +version = "24.11.1" description = "A library for working with the color formats defined by HTML and CSS." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["test"] files = [ - {file = "webcolors-24.8.0-py3-none-any.whl", hash = "sha256:fc4c3b59358ada164552084a8ebee637c221e4059267d0f8325b3b560f6c7f0a"}, - {file = "webcolors-24.8.0.tar.gz", hash = "sha256:08b07af286a01bcd30d583a7acadf629583d1f79bfef27dd2c2c5c263817277d"}, + {file = "webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9"}, + {file = "webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6"}, ] -[package.extras] -docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] -tests = ["coverage[toml]"] - [[package]] name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" optional = false python-versions = "*" +groups = ["test"] files = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, @@ -3985,6 +4549,7 @@ version = "1.8.0" description = "WebSocket client for Python with low level API options" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, @@ -3997,219 +4562,245 @@ test = ["websockets"] [[package]] name = "widgetsnbextension" -version = "4.0.13" +version = "4.0.14" description = "Jupyter interactive widgets for Jupyter Notebook" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "widgetsnbextension-4.0.13-py3-none-any.whl", hash = "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71"}, - {file = "widgetsnbextension-4.0.13.tar.gz", hash = "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6"}, + {file = "widgetsnbextension-4.0.14-py3-none-any.whl", hash = "sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575"}, + {file = "widgetsnbextension-4.0.14.tar.gz", hash = "sha256:a3629b04e3edb893212df862038c7232f62973373869db5084aed739b437b5af"}, ] [[package]] name = "wrapt" -version = "1.16.0" +version = "1.17.2" 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"}, +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, + {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, + {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, + {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, + {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, + {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, + {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, + {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, + {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, + {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, + {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, + {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, + {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, + {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, + {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, + {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, + {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, ] [[package]] name = "yarl" -version = "1.13.1" +version = "1.20.1" description = "Yet another URL library" optional = false -python-versions = ">=3.8" -files = [ - {file = "yarl-1.13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:82e692fb325013a18a5b73a4fed5a1edaa7c58144dc67ad9ef3d604eccd451ad"}, - {file = "yarl-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df4e82e68f43a07735ae70a2d84c0353e58e20add20ec0af611f32cd5ba43fb4"}, - {file = "yarl-1.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec9dd328016d8d25702a24ee274932aebf6be9787ed1c28d021945d264235b3c"}, - {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5820bd4178e6a639b3ef1db8b18500a82ceab6d8b89309e121a6859f56585b05"}, - {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86c438ce920e089c8c2388c7dcc8ab30dfe13c09b8af3d306bcabb46a053d6f7"}, - {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3de86547c820e4f4da4606d1c8ab5765dd633189791f15247706a2eeabc783ae"}, - {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca53632007c69ddcdefe1e8cbc3920dd88825e618153795b57e6ebcc92e752a"}, - {file = "yarl-1.13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4ee1d240b84e2f213565f0ec08caef27a0e657d4c42859809155cf3a29d1735"}, - {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c49f3e379177f4477f929097f7ed4b0622a586b0aa40c07ac8c0f8e40659a1ac"}, - {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5c5e32fef09ce101fe14acd0f498232b5710effe13abac14cd95de9c274e689e"}, - {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab9524e45ee809a083338a749af3b53cc7efec458c3ad084361c1dbf7aaf82a2"}, - {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b1481c048fe787f65e34cb06f7d6824376d5d99f1231eae4778bbe5c3831076d"}, - {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:31497aefd68036d8e31bfbacef915826ca2e741dbb97a8d6c7eac66deda3b606"}, - {file = "yarl-1.13.1-cp310-cp310-win32.whl", hash = "sha256:1fa56f34b2236f5192cb5fceba7bbb09620e5337e0b6dfe2ea0ddbd19dd5b154"}, - {file = "yarl-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:1bbb418f46c7f7355084833051701b2301092e4611d9e392360c3ba2e3e69f88"}, - {file = "yarl-1.13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:216a6785f296169ed52cd7dcdc2612f82c20f8c9634bf7446327f50398732a51"}, - {file = "yarl-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40c6e73c03a6befb85b72da213638b8aaa80fe4136ec8691560cf98b11b8ae6e"}, - {file = "yarl-1.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2430cf996113abe5aee387d39ee19529327205cda975d2b82c0e7e96e5fdabdc"}, - {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fb4134cc6e005b99fa29dbc86f1ea0a298440ab6b07c6b3ee09232a3b48f495"}, - {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309c104ecf67626c033845b860d31594a41343766a46fa58c3309c538a1e22b2"}, - {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f90575e9fe3aae2c1e686393a9689c724cd00045275407f71771ae5d690ccf38"}, - {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d2e1626be8712333a9f71270366f4a132f476ffbe83b689dd6dc0d114796c74"}, - {file = "yarl-1.13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b66c87da3c6da8f8e8b648878903ca54589038a0b1e08dde2c86d9cd92d4ac9"}, - {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cf1ad338620249f8dd6d4b6a91a69d1f265387df3697ad5dc996305cf6c26fb2"}, - {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9915300fe5a0aa663c01363db37e4ae8e7c15996ebe2c6cce995e7033ff6457f"}, - {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:703b0f584fcf157ef87816a3c0ff868e8c9f3c370009a8b23b56255885528f10"}, - {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1d8e3ca29f643dd121f264a7c89f329f0fcb2e4461833f02de6e39fef80f89da"}, - {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7055bbade838d68af73aea13f8c86588e4bcc00c2235b4b6d6edb0dbd174e246"}, - {file = "yarl-1.13.1-cp311-cp311-win32.whl", hash = "sha256:a3442c31c11088e462d44a644a454d48110f0588de830921fd201060ff19612a"}, - {file = "yarl-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:81bad32c8f8b5897c909bf3468bf601f1b855d12f53b6af0271963ee67fff0d2"}, - {file = "yarl-1.13.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f452cc1436151387d3d50533523291d5f77c6bc7913c116eb985304abdbd9ec9"}, - {file = "yarl-1.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9cec42a20eae8bebf81e9ce23fb0d0c729fc54cf00643eb251ce7c0215ad49fe"}, - {file = "yarl-1.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d959fe96e5c2712c1876d69af0507d98f0b0e8d81bee14cfb3f6737470205419"}, - {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8c837ab90c455f3ea8e68bee143472ee87828bff19ba19776e16ff961425b57"}, - {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94a993f976cdcb2dc1b855d8b89b792893220db8862d1a619efa7451817c836b"}, - {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2442a415a5f4c55ced0fade7b72123210d579f7d950e0b5527fc598866e62c"}, - {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fdbf0418489525231723cdb6c79e7738b3cbacbaed2b750cb033e4ea208f220"}, - {file = "yarl-1.13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b7f6e699304717fdc265a7e1922561b02a93ceffdaefdc877acaf9b9f3080b8"}, - {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bcd5bf4132e6a8d3eb54b8d56885f3d3a38ecd7ecae8426ecf7d9673b270de43"}, - {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2a93a4557f7fc74a38ca5a404abb443a242217b91cd0c4840b1ebedaad8919d4"}, - {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:22b739f99c7e4787922903f27a892744189482125cc7b95b747f04dd5c83aa9f"}, - {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2db874dd1d22d4c2c657807562411ffdfabec38ce4c5ce48b4c654be552759dc"}, - {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4feaaa4742517eaceafcbe74595ed335a494c84634d33961214b278126ec1485"}, - {file = "yarl-1.13.1-cp312-cp312-win32.whl", hash = "sha256:bbf9c2a589be7414ac4a534d54e4517d03f1cbb142c0041191b729c2fa23f320"}, - {file = "yarl-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:d07b52c8c450f9366c34aa205754355e933922c79135125541daae6cbf31c799"}, - {file = "yarl-1.13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:95c6737f28069153c399d875317f226bbdea939fd48a6349a3b03da6829fb550"}, - {file = "yarl-1.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cd66152561632ed4b2a9192e7f8e5a1d41e28f58120b4761622e0355f0fe034c"}, - {file = "yarl-1.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6a2acde25be0cf9be23a8f6cbd31734536a264723fca860af3ae5e89d771cd71"}, - {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18595e6a2ee0826bf7dfdee823b6ab55c9b70e8f80f8b77c37e694288f5de1"}, - {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a31d21089894942f7d9a8df166b495101b7258ff11ae0abec58e32daf8088813"}, - {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45f209fb4bbfe8630e3d2e2052535ca5b53d4ce2d2026bed4d0637b0416830da"}, - {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f722f30366474a99745533cc4015b1781ee54b08de73260b2bbe13316079851"}, - {file = "yarl-1.13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3bf60444269345d712838bb11cc4eadaf51ff1a364ae39ce87a5ca8ad3bb2c8"}, - {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:942c80a832a79c3707cca46bd12ab8aa58fddb34b1626d42b05aa8f0bcefc206"}, - {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:44b07e1690f010c3c01d353b5790ec73b2f59b4eae5b0000593199766b3f7a5c"}, - {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:396e59b8de7e4d59ff5507fb4322d2329865b909f29a7ed7ca37e63ade7f835c"}, - {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3bb83a0f12701c0b91112a11148b5217617982e1e466069d0555be9b372f2734"}, - {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c92b89bffc660f1274779cb6fbb290ec1f90d6dfe14492523a0667f10170de26"}, - {file = "yarl-1.13.1-cp313-cp313-win32.whl", hash = "sha256:269c201bbc01d2cbba5b86997a1e0f73ba5e2f471cfa6e226bcaa7fd664b598d"}, - {file = "yarl-1.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:1d0828e17fa701b557c6eaed5edbd9098eb62d8838344486248489ff233998b8"}, - {file = "yarl-1.13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8be8cdfe20787e6a5fcbd010f8066227e2bb9058331a4eccddec6c0db2bb85b2"}, - {file = "yarl-1.13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08d7148ff11cb8e886d86dadbfd2e466a76d5dd38c7ea8ebd9b0e07946e76e4b"}, - {file = "yarl-1.13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4afdf84610ca44dcffe8b6c22c68f309aff96be55f5ea2fa31c0c225d6b83e23"}, - {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0d12fe78dcf60efa205e9a63f395b5d343e801cf31e5e1dda0d2c1fb618073d"}, - {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298c1eecfd3257aa16c0cb0bdffb54411e3e831351cd69e6b0739be16b1bdaa8"}, - {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c14c16831b565707149c742d87a6203eb5597f4329278446d5c0ae7a1a43928e"}, - {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9bacedbb99685a75ad033fd4de37129449e69808e50e08034034c0bf063f99"}, - {file = "yarl-1.13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:658e8449b84b92a4373f99305de042b6bd0d19bf2080c093881e0516557474a5"}, - {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:373f16f38721c680316a6a00ae21cc178e3a8ef43c0227f88356a24c5193abd6"}, - {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:45d23c4668d4925688e2ea251b53f36a498e9ea860913ce43b52d9605d3d8177"}, - {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f7917697bcaa3bc3e83db91aa3a0e448bf5cde43c84b7fc1ae2427d2417c0224"}, - {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5989a38ba1281e43e4663931a53fbf356f78a0325251fd6af09dd03b1d676a09"}, - {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11b3ca8b42a024513adce810385fcabdd682772411d95bbbda3b9ed1a4257644"}, - {file = "yarl-1.13.1-cp38-cp38-win32.whl", hash = "sha256:dcaef817e13eafa547cdfdc5284fe77970b891f731266545aae08d6cce52161e"}, - {file = "yarl-1.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:7addd26594e588503bdef03908fc207206adac5bd90b6d4bc3e3cf33a829f57d"}, - {file = "yarl-1.13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a0ae6637b173d0c40b9c1462e12a7a2000a71a3258fa88756a34c7d38926911c"}, - {file = "yarl-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:576365c9f7469e1f6124d67b001639b77113cfd05e85ce0310f5f318fd02fe85"}, - {file = "yarl-1.13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78f271722423b2d4851cf1f4fa1a1c4833a128d020062721ba35e1a87154a049"}, - {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d74f3c335cfe9c21ea78988e67f18eb9822f5d31f88b41aec3a1ec5ecd32da5"}, - {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1891d69a6ba16e89473909665cd355d783a8a31bc84720902c5911dbb6373465"}, - {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb382fd7b4377363cc9f13ba7c819c3c78ed97c36a82f16f3f92f108c787cbbf"}, - {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8854b9f80693d20cec797d8e48a848c2fb273eb6f2587b57763ccba3f3bd4b"}, - {file = "yarl-1.13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbf2c3f04ff50f16404ce70f822cdc59760e5e2d7965905f0e700270feb2bbfc"}, - {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fb9f59f3848edf186a76446eb8bcf4c900fe147cb756fbbd730ef43b2e67c6a7"}, - {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ef9b85fa1bc91c4db24407e7c4da93a5822a73dd4513d67b454ca7064e8dc6a3"}, - {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:098b870c18f1341786f290b4d699504e18f1cd050ed179af8123fd8232513424"}, - {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8c723c91c94a3bc8033dd2696a0f53e5d5f8496186013167bddc3fb5d9df46a3"}, - {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44a4c40a6f84e4d5955b63462a0e2a988f8982fba245cf885ce3be7618f6aa7d"}, - {file = "yarl-1.13.1-cp39-cp39-win32.whl", hash = "sha256:84bbcdcf393139f0abc9f642bf03f00cac31010f3034faa03224a9ef0bb74323"}, - {file = "yarl-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:fc2931ac9ce9c61c9968989ec831d3a5e6fcaaff9474e7cfa8de80b7aff5a093"}, - {file = "yarl-1.13.1-py3-none-any.whl", hash = "sha256:6a5185ad722ab4dd52d5fb1f30dcc73282eb1ed494906a92d1a228d3f89607b0"}, - {file = "yarl-1.13.1.tar.gz", hash = "sha256:ec8cfe2295f3e5e44c51f57272afbd69414ae629ec7c6b27f5a410efc78b70a0"}, +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4"}, + {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a"}, + {file = "yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13"}, + {file = "yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8"}, + {file = "yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e"}, + {file = "yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773"}, + {file = "yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004"}, + {file = "yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5"}, + {file = "yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1"}, + {file = "yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7"}, + {file = "yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e"}, + {file = "yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d"}, + {file = "yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d"}, + {file = "yarl-1.20.1-cp39-cp39-win32.whl", hash = "sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06"}, + {file = "yarl-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00"}, + {file = "yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77"}, + {file = "yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" +propcache = ">=0.2.1" [[package]] name = "zipp" -version = "3.20.2" +version = "3.23.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [metadata] -lock-version = "2.0" -python-versions = "^3.9" -content-hash = "a8bc47ba5ed0d0d30a52b50bf986b85da8cdc8b85f9dbe47ebae32dab695e95f" +lock-version = "2.1" +python-versions = "^3.11" +content-hash = "1a3968dbde8f4356b4d93b17f5bcf75f2bc38587553273742de05d9f0f6ee87c" diff --git a/pyproject.toml b/pyproject.toml index ef88b255..0a9d3228 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,23 @@ +[project] +name = "codeflare-sdk" +version = "0.32.0" + [tool.poetry] name = "codeflare-sdk" -version = "0.0.0-dev" +version = "0.32.0" description = "Python SDK for codeflare client" license = "Apache-2.0" +# Exclude vendored tests, examples, and build files from the package +exclude = [ + "src/codeflare_sdk/vendored/python_client_test", + "src/codeflare_sdk/vendored/examples", + "src/codeflare_sdk/vendored/pyproject.toml", + "src/codeflare_sdk/vendored/poetry.lock", + "src/codeflare_sdk/vendored/README.md" +] + authors = [ "Michael Clifford ", "Mustafa Eyceoz ", @@ -20,16 +33,23 @@ homepage = "https://github.com/project-codeflare/codeflare-sdk" keywords = ['codeflare', 'python', 'sdk', 'client', 'batch', 'scale'] [tool.poetry.dependencies] -python = "^3.9" +python = "^3.11" openshift-client = "1.0.18" rich = ">=12.5,<14.0" -ray = {version = "2.35.0", extras = ["data", "default"]} -kubernetes = ">= 25.3.0, < 27" +ray = {version = "2.47.1", extras = ["data", "default"]} +kubernetes = ">= 27.2.0" cryptography = "43.0.3" executing = "1.2.0" -pydantic = "< 2" +pydantic = ">= 2.10.6" ipywidgets = "8.1.2" +[[tool.poetry.source]] +name = "pypi" + +[[tool.poetry.source]] +name = "testpypi" +url = "https://test.pypi.org/simple/" + [tool.poetry.group.docs] optional = true @@ -47,6 +67,10 @@ pytest-mock = "3.11.1" pytest-timeout = "2.3.1" jupyterlab = "4.3.1" + +[tool.poetry.group.dev.dependencies] +diff-cover = "^9.6.0" + [tool.pytest.ini_options] filterwarnings = [ "ignore::DeprecationWarning:pkg_resources", @@ -57,6 +81,10 @@ markers = [ "openshift", "nvidia_gpu" ] -addopts = "--timeout=900" +addopts = "--timeout=900 --ignore=src/codeflare_sdk/vendored" testpaths = ["src/codeflare_sdk"] collect_ignore = ["src/codeflare_sdk/common/utils/unit_test_support.py"] + +[build-system] +requires = ["poetry-core>=1.6.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/codeflare_sdk/__init__.py b/src/codeflare_sdk/__init__.py index 9ab5c745..a27702e7 100644 --- a/src/codeflare_sdk/__init__.py +++ b/src/codeflare_sdk/__init__.py @@ -10,6 +10,8 @@ AWManager, AppWrapperStatus, RayJobClient, + RayJob, + ManagedClusterConfig, ) from .common.widgets import view_clusters diff --git a/src/codeflare_sdk/common/kueue/kueue.py b/src/codeflare_sdk/common/kueue/kueue.py index 00f3364a..a721713e 100644 --- a/src/codeflare_sdk/common/kueue/kueue.py +++ b/src/codeflare_sdk/common/kueue/kueue.py @@ -18,6 +18,8 @@ from kubernetes import client from kubernetes.client.exceptions import ApiException +from ...common.utils import get_current_namespace + def get_default_kueue_name(namespace: str) -> Optional[str]: """ @@ -81,7 +83,6 @@ def list_local_queues( List[dict]: A list of dictionaries containing the name of the local queue and the available flavors """ - from ...ray.cluster.cluster import get_current_namespace if namespace is None: # pragma: no cover namespace = get_current_namespace() diff --git a/src/codeflare_sdk/common/kueue/test_kueue.py b/src/codeflare_sdk/common/kueue/test_kueue.py index 77095d4d..bbc54e9e 100644 --- a/src/codeflare_sdk/common/kueue/test_kueue.py +++ b/src/codeflare_sdk/common/kueue/test_kueue.py @@ -11,14 +11,19 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from ..utils.unit_test_support import get_local_queue, createClusterConfig +from ..utils.unit_test_support import ( + get_local_queue, + create_cluster_config, + get_template_variables, + apply_template, +) from unittest.mock import patch from codeflare_sdk.ray.cluster.cluster import Cluster, ClusterConfiguration import yaml import os import filecmp from pathlib import Path -from .kueue import list_local_queues +from .kueue import list_local_queues, local_queue_exists, add_queue_label parent = Path(__file__).resolve().parents[4] # project directory aw_dir = os.path.expanduser("~/.codeflare/resources/") @@ -46,27 +51,27 @@ def test_cluster_creation_no_aw_local_queue(mocker): "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), ) - config = createClusterConfig() + config = create_cluster_config() config.name = "unit-test-cluster-kueue" config.write_to_file = True config.local_queue = "local-queue-default" cluster = Cluster(config) assert cluster.resource_yaml == f"{aw_dir}unit-test-cluster-kueue.yaml" - assert filecmp.cmp( - f"{aw_dir}unit-test-cluster-kueue.yaml", + expected_rc = apply_template( f"{parent}/tests/test_cluster_yamls/kueue/ray_cluster_kueue.yaml", - shallow=True, + get_template_variables(), ) + with open(f"{aw_dir}unit-test-cluster-kueue.yaml", "r") as f: + cluster_kueue = yaml.load(f, Loader=yaml.FullLoader) + assert cluster_kueue == expected_rc + # With resources loaded in memory, no Local Queue specified. - config = createClusterConfig() + config = create_cluster_config() config.name = "unit-test-cluster-kueue" config.write_to_file = False cluster = Cluster(config) - - with open(f"{parent}/tests/test_cluster_yamls/kueue/ray_cluster_kueue.yaml") as f: - expected_rc = yaml.load(f, Loader=yaml.FullLoader) - assert cluster.resource_yaml == expected_rc + assert cluster.resource_yaml == expected_rc def test_aw_creation_local_queue(mocker): @@ -79,29 +84,30 @@ def test_aw_creation_local_queue(mocker): "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), ) - config = createClusterConfig() + config = create_cluster_config() config.name = "unit-test-aw-kueue" config.appwrapper = True config.write_to_file = True config.local_queue = "local-queue-default" cluster = Cluster(config) assert cluster.resource_yaml == f"{aw_dir}unit-test-aw-kueue.yaml" - assert filecmp.cmp( - f"{aw_dir}unit-test-aw-kueue.yaml", + expected_rc = apply_template( f"{parent}/tests/test_cluster_yamls/kueue/aw_kueue.yaml", - shallow=True, + get_template_variables(), ) + with open(f"{aw_dir}unit-test-aw-kueue.yaml", "r") as f: + aw_kueue = yaml.load(f, Loader=yaml.FullLoader) + assert aw_kueue == expected_rc + # With resources loaded in memory, no Local Queue specified. - config = createClusterConfig() + config = create_cluster_config() config.name = "unit-test-aw-kueue" config.appwrapper = True config.write_to_file = False cluster = Cluster(config) - with open(f"{parent}/tests/test_cluster_yamls/kueue/aw_kueue.yaml") as f: - expected_rc = yaml.load(f, Loader=yaml.FullLoader) - assert cluster.resource_yaml == expected_rc + assert cluster.resource_yaml == expected_rc def test_get_local_queue_exists_fail(mocker): @@ -114,7 +120,7 @@ def test_get_local_queue_exists_fail(mocker): "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), ) - config = createClusterConfig() + config = create_cluster_config() config.name = "unit-test-aw-kueue" config.appwrapper = True config.write_to_file = True @@ -169,6 +175,123 @@ def test_list_local_queues(mocker): assert lqs == [] +def test_local_queue_exists_found(mocker): + # Mock Kubernetes client and list_namespaced_custom_object method + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mock_api_instance = mocker.Mock() + mocker.patch("kubernetes.client.CustomObjectsApi", return_value=mock_api_instance) + mocker.patch("codeflare_sdk.ray.cluster.cluster.config_check") + + # Mock return value for list_namespaced_custom_object + mock_api_instance.list_namespaced_custom_object.return_value = { + "items": [ + {"metadata": {"name": "existing-queue"}}, + {"metadata": {"name": "another-queue"}}, + ] + } + + # Call the function + namespace = "test-namespace" + local_queue_name = "existing-queue" + result = local_queue_exists(namespace, local_queue_name) + + # Assertions + assert result is True + mock_api_instance.list_namespaced_custom_object.assert_called_once_with( + group="kueue.x-k8s.io", + version="v1beta1", + namespace=namespace, + plural="localqueues", + ) + + +def test_local_queue_exists_not_found(mocker): + # Mock Kubernetes client and list_namespaced_custom_object method + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mock_api_instance = mocker.Mock() + mocker.patch("kubernetes.client.CustomObjectsApi", return_value=mock_api_instance) + mocker.patch("codeflare_sdk.ray.cluster.cluster.config_check") + + # Mock return value for list_namespaced_custom_object + mock_api_instance.list_namespaced_custom_object.return_value = { + "items": [ + {"metadata": {"name": "another-queue"}}, + {"metadata": {"name": "different-queue"}}, + ] + } + + # Call the function + namespace = "test-namespace" + local_queue_name = "non-existent-queue" + result = local_queue_exists(namespace, local_queue_name) + + # Assertions + assert result is False + mock_api_instance.list_namespaced_custom_object.assert_called_once_with( + group="kueue.x-k8s.io", + version="v1beta1", + namespace=namespace, + plural="localqueues", + ) + + +import pytest +from unittest import mock # If you're also using mocker from pytest-mock + + +def test_add_queue_label_with_valid_local_queue(mocker): + # Mock the kubernetes.client.CustomObjectsApi and its response + mock_api_instance = mocker.patch("kubernetes.client.CustomObjectsApi") + mock_api_instance.return_value.list_namespaced_custom_object.return_value = { + "items": [ + {"metadata": {"name": "valid-queue"}}, + ] + } + + # Mock other dependencies + mocker.patch("codeflare_sdk.common.kueue.local_queue_exists", return_value=True) + mocker.patch( + "codeflare_sdk.common.kueue.get_default_kueue_name", + return_value="default-queue", + ) + + # Define input item and parameters + item = {"metadata": {}} + namespace = "test-namespace" + local_queue = "valid-queue" + + # Call the function + add_queue_label(item, namespace, local_queue) + + # Assert that the label is added to the item + assert item["metadata"]["labels"] == {"kueue.x-k8s.io/queue-name": "valid-queue"} + + +def test_add_queue_label_with_invalid_local_queue(mocker): + # Mock the kubernetes.client.CustomObjectsApi and its response + mock_api_instance = mocker.patch("kubernetes.client.CustomObjectsApi") + mock_api_instance.return_value.list_namespaced_custom_object.return_value = { + "items": [ + {"metadata": {"name": "valid-queue"}}, + ] + } + + # Mock the local_queue_exists function to return False + mocker.patch("codeflare_sdk.common.kueue.local_queue_exists", return_value=False) + + # Define input item and parameters + item = {"metadata": {}} + namespace = "test-namespace" + local_queue = "invalid-queue" + + # Call the function and expect a ValueError + with pytest.raises( + ValueError, + match="local_queue provided does not exist or is not in this namespace", + ): + add_queue_label(item, namespace, local_queue) + + # Make sure to always keep this function last def test_cleanup(): os.remove(f"{aw_dir}unit-test-cluster-kueue.yaml") diff --git a/src/codeflare_sdk/common/utils/__init__.py b/src/codeflare_sdk/common/utils/__init__.py index e69de29b..e662bf5e 100644 --- a/src/codeflare_sdk/common/utils/__init__.py +++ b/src/codeflare_sdk/common/utils/__init__.py @@ -0,0 +1,7 @@ +""" +Common utilities for the CodeFlare SDK. +""" + +from .k8s_utils import get_current_namespace + +__all__ = ["get_current_namespace"] diff --git a/src/codeflare_sdk/common/utils/constants.py b/src/codeflare_sdk/common/utils/constants.py new file mode 100644 index 00000000..00559d2e --- /dev/null +++ b/src/codeflare_sdk/common/utils/constants.py @@ -0,0 +1,15 @@ +RAY_VERSION = "2.47.1" +""" +The below are used to define the default runtime image for the Ray Cluster. +* For python 3.11:ray:2.47.1-py311-cu121 +* For python 3.12:ray:2.47.1-py312-cu128 +""" +CUDA_PY311_RUNTIME_IMAGE = "quay.io/modh/ray@sha256:6d076aeb38ab3c34a6a2ef0f58dc667089aa15826fa08a73273c629333e12f1e" +CUDA_PY312_RUNTIME_IMAGE = "quay.io/modh/ray@sha256:fb6f207de63e442c67bb48955cf0584f3704281faf17b90419cfa274fdec63c5" + +# Centralized image selection +SUPPORTED_PYTHON_VERSIONS = { + "3.11": CUDA_PY311_RUNTIME_IMAGE, + "3.12": CUDA_PY312_RUNTIME_IMAGE, +} +MOUNT_PATH = "/home/ray/files" diff --git a/src/codeflare_sdk/common/utils/k8s_utils.py b/src/codeflare_sdk/common/utils/k8s_utils.py new file mode 100644 index 00000000..e2e03a5d --- /dev/null +++ b/src/codeflare_sdk/common/utils/k8s_utils.py @@ -0,0 +1,33 @@ +""" +Kubernetes utility functions for the CodeFlare SDK. +""" + +import os +from kubernetes import config +from ..kubernetes_cluster import config_check, _kube_api_error_handling + + +def get_current_namespace(): # pragma: no cover + """ + Retrieves the current Kubernetes namespace. + + Returns: + str: + The current namespace or None if not found. + """ + if os.path.isfile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"): + try: + file = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") + active_context = file.readline().strip("\n") + return active_context + except Exception as e: + print("Unable to find current namespace") + print("trying to gather from current context") + try: + _, active_context = config.list_kube_config_contexts(config_check()) + except Exception as e: + return _kube_api_error_handling(e) + try: + return active_context["context"]["namespace"] + except KeyError: + return None diff --git a/src/codeflare_sdk/common/utils/test_demos.py b/src/codeflare_sdk/common/utils/test_demos.py new file mode 100644 index 00000000..9124cbec --- /dev/null +++ b/src/codeflare_sdk/common/utils/test_demos.py @@ -0,0 +1,57 @@ +# Copyright 2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for demos module. +""" + +import pytest +import tempfile +from pathlib import Path +from unittest.mock import patch, MagicMock +from codeflare_sdk.common.utils.demos import copy_demo_nbs + + +class TestCopyDemoNbs: + """Test cases for copy_demo_nbs function.""" + + def test_copy_demo_nbs_directory_exists_error(self): + """Test that FileExistsError is raised when directory exists and overwrite=False.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create a subdirectory that will conflict + conflict_dir = Path(temp_dir) / "demo-notebooks" + conflict_dir.mkdir() + + with pytest.raises(FileExistsError, match="Directory.*already exists"): + copy_demo_nbs(dir=str(conflict_dir), overwrite=False) + + def test_copy_demo_nbs_overwrite_true(self): + """Test that overwrite=True allows copying to existing directory.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create a subdirectory that will conflict + conflict_dir = Path(temp_dir) / "demo-notebooks" + conflict_dir.mkdir() + + # Mock the demo_dir to point to a real directory + with patch("codeflare_sdk.common.utils.demos.demo_dir", temp_dir): + # Should not raise an error with overwrite=True + copy_demo_nbs(dir=str(conflict_dir), overwrite=True) + + def test_copy_demo_nbs_default_parameters(self): + """Test copy_demo_nbs with default parameters.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Mock the demo_dir to point to a real directory + with patch("codeflare_sdk.common.utils.demos.demo_dir", temp_dir): + # Should work with default parameters + copy_demo_nbs(dir=temp_dir, overwrite=True) diff --git a/src/codeflare_sdk/common/utils/test_k8s_utils.py b/src/codeflare_sdk/common/utils/test_k8s_utils.py new file mode 100644 index 00000000..fcd0623d --- /dev/null +++ b/src/codeflare_sdk/common/utils/test_k8s_utils.py @@ -0,0 +1,255 @@ +# Copyright 2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for k8s_utils module. +""" + +import pytest +from unittest.mock import mock_open, patch, MagicMock +from codeflare_sdk.common.utils.k8s_utils import get_current_namespace + + +class TestGetCurrentNamespace: + """Test cases for get_current_namespace function.""" + + def test_get_current_namespace_incluster_success(self): + """Test successful namespace detection from in-cluster service account.""" + mock_file_content = "test-namespace\n" + + with patch("os.path.isfile", return_value=True): + with patch("builtins.open", mock_open(read_data=mock_file_content)): + result = get_current_namespace() + + assert result == "test-namespace" + + def test_get_current_namespace_incluster_file_read_error(self): + """Test handling of file read errors when reading service account namespace.""" + with patch("os.path.isfile", return_value=True): + with patch("builtins.open", side_effect=IOError("File read error")): + with patch("builtins.print") as mock_print: + # Mock config_check to avoid kubeconfig fallback + with patch( + "codeflare_sdk.common.utils.k8s_utils.config_check", + side_effect=Exception("Config error"), + ): + with patch( + "codeflare_sdk.common.utils.k8s_utils._kube_api_error_handling", + return_value=None, + ): + result = get_current_namespace() + + assert result is None + # Should see both error messages: in-cluster failure and kubeconfig fallback + mock_print.assert_any_call("Unable to find current namespace") + mock_print.assert_any_call("trying to gather from current context") + + def test_get_current_namespace_incluster_file_open_error(self): + """Test handling of file open errors when reading service account namespace.""" + with patch("os.path.isfile", return_value=True): + with patch( + "builtins.open", side_effect=PermissionError("Permission denied") + ): + with patch("builtins.print") as mock_print: + # Mock config_check to avoid kubeconfig fallback + with patch( + "codeflare_sdk.common.utils.k8s_utils.config_check", + side_effect=Exception("Config error"), + ): + with patch( + "codeflare_sdk.common.utils.k8s_utils._kube_api_error_handling", + return_value=None, + ): + result = get_current_namespace() + + assert result is None + # Should see both error messages: in-cluster failure and kubeconfig fallback + mock_print.assert_any_call("Unable to find current namespace") + mock_print.assert_any_call("trying to gather from current context") + + def test_get_current_namespace_kubeconfig_success(self): + """Test successful namespace detection from kubeconfig context.""" + mock_contexts = [ + {"name": "context1", "context": {"namespace": "default"}}, + {"name": "context2", "context": {"namespace": "test-namespace"}}, + ] + mock_active_context = { + "name": "context2", + "context": {"namespace": "test-namespace"}, + } + + with patch("os.path.isfile", return_value=False): + with patch("builtins.print") as mock_print: + with patch( + "codeflare_sdk.common.utils.k8s_utils.config_check", + return_value="~/.kube/config", + ): + with patch( + "kubernetes.config.list_kube_config_contexts", + return_value=(mock_contexts, mock_active_context), + ): + result = get_current_namespace() + + assert result == "test-namespace" + mock_print.assert_called_with("trying to gather from current context") + + def test_get_current_namespace_kubeconfig_no_namespace_in_context(self): + """Test handling when kubeconfig context has no namespace field.""" + mock_contexts = [ + {"name": "context1", "context": {}}, + {"name": "context2", "context": {"cluster": "test-cluster"}}, + ] + mock_active_context = { + "name": "context2", + "context": {"cluster": "test-cluster"}, + } + + with patch("os.path.isfile", return_value=False): + with patch("builtins.print") as mock_print: + with patch( + "codeflare_sdk.common.utils.k8s_utils.config_check", + return_value="~/.kube/config", + ): + with patch( + "kubernetes.config.list_kube_config_contexts", + return_value=(mock_contexts, mock_active_context), + ): + result = get_current_namespace() + + assert result is None + mock_print.assert_called_with("trying to gather from current context") + + def test_get_current_namespace_kubeconfig_config_check_error(self): + """Test handling when config_check raises an exception.""" + with patch("os.path.isfile", return_value=False): + with patch("builtins.print") as mock_print: + with patch( + "codeflare_sdk.common.utils.k8s_utils.config_check", + side_effect=Exception("Config error"), + ): + with patch( + "codeflare_sdk.common.utils.k8s_utils._kube_api_error_handling", + return_value=None, + ) as mock_error_handler: + result = get_current_namespace() + + assert result is None + mock_print.assert_called_with("trying to gather from current context") + mock_error_handler.assert_called_once() + + def test_get_current_namespace_kubeconfig_list_contexts_error(self): + """Test handling when list_kube_config_contexts raises an exception.""" + with patch("os.path.isfile", return_value=False): + with patch("builtins.print") as mock_print: + with patch( + "codeflare_sdk.common.utils.k8s_utils.config_check", + return_value="~/.kube/config", + ): + with patch( + "kubernetes.config.list_kube_config_contexts", + side_effect=Exception("Context error"), + ): + with patch( + "codeflare_sdk.common.utils.k8s_utils._kube_api_error_handling", + return_value=None, + ) as mock_error_handler: + result = get_current_namespace() + + assert result is None + mock_print.assert_called_with("trying to gather from current context") + mock_error_handler.assert_called_once() + + def test_get_current_namespace_kubeconfig_key_error(self): + """Test handling when accessing context namespace raises KeyError.""" + mock_contexts = [{"name": "context1", "context": {"namespace": "default"}}] + mock_active_context = {"name": "context1"} # Missing 'context' key + + with patch("os.path.isfile", return_value=False): + with patch("builtins.print") as mock_print: + with patch( + "codeflare_sdk.common.utils.k8s_utils.config_check", + return_value="~/.kube/config", + ): + with patch( + "kubernetes.config.list_kube_config_contexts", + return_value=(mock_contexts, mock_active_context), + ): + result = get_current_namespace() + + assert result is None + mock_print.assert_called_with("trying to gather from current context") + + def test_get_current_namespace_fallback_flow(self): + """Test the complete fallback flow from in-cluster to kubeconfig.""" + # First attempt: in-cluster file doesn't exist + # Second attempt: kubeconfig context has namespace + mock_contexts = [ + {"name": "context1", "context": {"namespace": "fallback-namespace"}} + ] + mock_active_context = { + "name": "context1", + "context": {"namespace": "fallback-namespace"}, + } + + with patch("os.path.isfile", return_value=False): + with patch("builtins.print") as mock_print: + with patch( + "codeflare_sdk.common.utils.k8s_utils.config_check", + return_value="~/.kube/config", + ): + with patch( + "kubernetes.config.list_kube_config_contexts", + return_value=(mock_contexts, mock_active_context), + ): + result = get_current_namespace() + + assert result == "fallback-namespace" + mock_print.assert_called_with("trying to gather from current context") + + def test_get_current_namespace_complete_failure(self): + """Test complete failure scenario where no namespace can be detected.""" + with patch("os.path.isfile", return_value=False): + with patch("builtins.print") as mock_print: + with patch( + "codeflare_sdk.common.utils.k8s_utils.config_check", + side_effect=Exception("Config error"), + ): + with patch( + "codeflare_sdk.common.utils.k8s_utils._kube_api_error_handling", + return_value=None, + ): + result = get_current_namespace() + + assert result is None + mock_print.assert_called_with("trying to gather from current context") + + def test_get_current_namespace_mixed_errors(self): + """Test scenario with mixed error conditions.""" + # In-cluster file exists but read fails, then kubeconfig also fails + with patch("os.path.isfile", return_value=True): + with patch("builtins.open", side_effect=IOError("File read error")): + with patch("builtins.print") as mock_print: + with patch( + "codeflare_sdk.common.utils.k8s_utils.config_check", + side_effect=Exception("Config error"), + ): + with patch( + "codeflare_sdk.common.utils.k8s_utils._kube_api_error_handling", + return_value=None, + ): + result = get_current_namespace() + + assert result is None + # Should see both error messages + assert mock_print.call_count >= 2 diff --git a/src/codeflare_sdk/common/utils/test_utils.py b/src/codeflare_sdk/common/utils/test_utils.py new file mode 100644 index 00000000..d330bc4d --- /dev/null +++ b/src/codeflare_sdk/common/utils/test_utils.py @@ -0,0 +1,209 @@ +# Copyright 2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for common/utils/utils.py +""" + +import pytest +from collections import namedtuple +from codeflare_sdk.common.utils.utils import ( + update_image, + get_ray_image_for_python_version, +) +from codeflare_sdk.common.utils.constants import ( + SUPPORTED_PYTHON_VERSIONS, + CUDA_PY311_RUNTIME_IMAGE, + CUDA_PY312_RUNTIME_IMAGE, +) + + +def test_update_image_with_empty_string_python_311(mocker): + """Test that update_image() with empty string returns default image for Python 3.11.""" + # Mock sys.version_info to simulate Python 3.11 + VersionInfo = namedtuple( + "version_info", ["major", "minor", "micro", "releaselevel", "serial"] + ) + mocker.patch("sys.version_info", VersionInfo(3, 11, 0, "final", 0)) + + # Test with empty image (should use default for Python 3.11) + image = update_image("") + assert image == CUDA_PY311_RUNTIME_IMAGE + assert image == SUPPORTED_PYTHON_VERSIONS["3.11"] + + +def test_update_image_with_empty_string_python_312(mocker): + """Test that update_image() with empty string returns default image for Python 3.12.""" + # Mock sys.version_info to simulate Python 3.12 + VersionInfo = namedtuple( + "version_info", ["major", "minor", "micro", "releaselevel", "serial"] + ) + mocker.patch("sys.version_info", VersionInfo(3, 12, 0, "final", 0)) + + # Test with empty image (should use default for Python 3.12) + image = update_image("") + assert image == CUDA_PY312_RUNTIME_IMAGE + assert image == SUPPORTED_PYTHON_VERSIONS["3.12"] + + +def test_update_image_with_none_python_311(mocker): + """Test that update_image() with None returns default image for Python 3.11.""" + # Mock sys.version_info to simulate Python 3.11 + VersionInfo = namedtuple( + "version_info", ["major", "minor", "micro", "releaselevel", "serial"] + ) + mocker.patch("sys.version_info", VersionInfo(3, 11, 0, "final", 0)) + + # Test with None image (should use default for Python 3.11) + image = update_image(None) + assert image == CUDA_PY311_RUNTIME_IMAGE + + +def test_update_image_with_none_python_312(mocker): + """Test that update_image() with None returns default image for Python 3.12.""" + # Mock sys.version_info to simulate Python 3.12 + VersionInfo = namedtuple( + "version_info", ["major", "minor", "micro", "releaselevel", "serial"] + ) + mocker.patch("sys.version_info", VersionInfo(3, 12, 0, "final", 0)) + + # Test with None image (should use default for Python 3.12) + image = update_image(None) + assert image == CUDA_PY312_RUNTIME_IMAGE + + +def test_update_image_with_unsupported_python_version(mocker): + """Test update_image() warning for unsupported Python versions.""" + # Mock sys.version_info to simulate Python 3.8 (unsupported) + VersionInfo = namedtuple( + "version_info", ["major", "minor", "micro", "releaselevel", "serial"] + ) + mocker.patch("sys.version_info", VersionInfo(3, 8, 0, "final", 0)) + + # Mock warnings.warn to check if it gets called + warn_mock = mocker.patch("warnings.warn") + + # Call update_image with empty image + image = update_image("") + + # Assert that the warning was called with the expected message + warn_mock.assert_called_once() + assert "No default Ray image defined for 3.8" in warn_mock.call_args[0][0] + assert "3.11, 3.12" in warn_mock.call_args[0][0] + + # Assert that no image was set since the Python version is not supported + assert image is None + + +def test_update_image_with_provided_custom_image(): + """Test that providing a custom image bypasses auto-detection.""" + custom_image = "my-custom-ray:latest" + image = update_image(custom_image) + + # Should return the provided image unchanged + assert image == custom_image + + +def test_update_image_with_provided_image_empty_string(): + """Test update_image() with provided custom image as a non-empty string.""" + custom_image = "docker.io/rayproject/ray:2.40.0" + image = update_image(custom_image) + + # Should return the provided image unchanged + assert image == custom_image + + +def test_get_ray_image_for_python_version_explicit_311(): + """Test get_ray_image_for_python_version() with explicit Python 3.11.""" + image = get_ray_image_for_python_version("3.11") + assert image == CUDA_PY311_RUNTIME_IMAGE + + +def test_get_ray_image_for_python_version_explicit_312(): + """Test get_ray_image_for_python_version() with explicit Python 3.12.""" + image = get_ray_image_for_python_version("3.12") + assert image == CUDA_PY312_RUNTIME_IMAGE + + +def test_get_ray_image_for_python_version_auto_detect_311(mocker): + """Test get_ray_image_for_python_version() auto-detects Python 3.11.""" + # Mock sys.version_info to simulate Python 3.11 + VersionInfo = namedtuple( + "version_info", ["major", "minor", "micro", "releaselevel", "serial"] + ) + mocker.patch("sys.version_info", VersionInfo(3, 11, 0, "final", 0)) + + # Test with None (should auto-detect) + image = get_ray_image_for_python_version() + assert image == CUDA_PY311_RUNTIME_IMAGE + + +def test_get_ray_image_for_python_version_auto_detect_312(mocker): + """Test get_ray_image_for_python_version() auto-detects Python 3.12.""" + # Mock sys.version_info to simulate Python 3.12 + VersionInfo = namedtuple( + "version_info", ["major", "minor", "micro", "releaselevel", "serial"] + ) + mocker.patch("sys.version_info", VersionInfo(3, 12, 0, "final", 0)) + + # Test with None (should auto-detect) + image = get_ray_image_for_python_version() + assert image == CUDA_PY312_RUNTIME_IMAGE + + +def test_get_ray_image_for_python_version_unsupported_with_warning(mocker): + """Test get_ray_image_for_python_version() warns for unsupported versions.""" + warn_mock = mocker.patch("warnings.warn") + + # Test with unsupported version and warn_on_unsupported=True (default) + image = get_ray_image_for_python_version("3.9", warn_on_unsupported=True) + + # Should have warned + warn_mock.assert_called_once() + assert "No default Ray image defined for 3.9" in warn_mock.call_args[0][0] + + # Should return None + assert image is None + + +def test_get_ray_image_for_python_version_unsupported_without_warning(): + """Test get_ray_image_for_python_version() falls back to 3.12 without warning.""" + # Test with unsupported version and warn_on_unsupported=False + image = get_ray_image_for_python_version("3.10", warn_on_unsupported=False) + + # Should fall back to Python 3.12 image + assert image == CUDA_PY312_RUNTIME_IMAGE + + +def test_get_ray_image_for_python_version_unsupported_silent_fallback(): + """Test get_ray_image_for_python_version() silently falls back for old versions.""" + # Test with Python 3.8 and warn_on_unsupported=False + image = get_ray_image_for_python_version("3.8", warn_on_unsupported=False) + + # Should fall back to Python 3.12 image without warning + assert image == CUDA_PY312_RUNTIME_IMAGE + + +def test_get_ray_image_for_python_version_none_defaults_to_current(mocker): + """Test that passing None to get_ray_image_for_python_version() uses current Python.""" + # Mock sys.version_info to simulate Python 3.11 + VersionInfo = namedtuple( + "version_info", ["major", "minor", "micro", "releaselevel", "serial"] + ) + mocker.patch("sys.version_info", VersionInfo(3, 11, 5, "final", 0)) + + # Passing None should detect the mocked version + image = get_ray_image_for_python_version(None, warn_on_unsupported=True) + + assert image == CUDA_PY311_RUNTIME_IMAGE diff --git a/src/codeflare_sdk/common/utils/test_validation.py b/src/codeflare_sdk/common/utils/test_validation.py new file mode 100644 index 00000000..20416d00 --- /dev/null +++ b/src/codeflare_sdk/common/utils/test_validation.py @@ -0,0 +1,224 @@ +# Copyright 2022-2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from codeflare_sdk.common.utils.validation import ( + extract_ray_version_from_image, + validate_ray_version_compatibility, +) +from codeflare_sdk.common.utils.constants import RAY_VERSION + + +class TestRayVersionDetection: + """Test Ray version detection from container image names.""" + + def test_extract_ray_version_standard_format(self): + """Test extraction from standard Ray image formats.""" + # Standard format + assert extract_ray_version_from_image("ray:2.47.1") == "2.47.1" + assert extract_ray_version_from_image("ray:2.46.0") == "2.46.0" + assert extract_ray_version_from_image("ray:1.13.0") == "1.13.0" + + def test_extract_ray_version_with_registry(self): + """Test extraction from images with registry prefixes.""" + assert extract_ray_version_from_image("quay.io/ray:2.47.1") == "2.47.1" + assert ( + extract_ray_version_from_image("docker.io/rayproject/ray:2.47.1") + == "2.47.1" + ) + assert ( + extract_ray_version_from_image("gcr.io/my-project/ray:2.47.1") == "2.47.1" + ) + + def test_extract_ray_version_with_suffixes(self): + """Test extraction from images with version suffixes.""" + assert ( + extract_ray_version_from_image("quay.io/modh/ray:2.47.1-py311-cu121") + == "2.47.1" + ) + assert extract_ray_version_from_image("ray:2.47.1-py311") == "2.47.1" + assert extract_ray_version_from_image("ray:2.47.1-gpu") == "2.47.1" + assert extract_ray_version_from_image("ray:2.47.1-rocm62") == "2.47.1" + + def test_extract_ray_version_complex_registry_paths(self): + """Test extraction from complex registry paths.""" + assert ( + extract_ray_version_from_image("quay.io/modh/ray:2.47.1-py311-cu121") + == "2.47.1" + ) + assert ( + extract_ray_version_from_image("registry.company.com/team/ray:2.47.1") + == "2.47.1" + ) + + def test_extract_ray_version_no_version_found(self): + """Test cases where no version can be extracted.""" + # SHA-based tags + assert ( + extract_ray_version_from_image( + "quay.io/modh/ray@sha256:6d076aeb38ab3c34a6a2ef0f58dc667089aa15826fa08a73273c629333e12f1e" + ) + is None + ) + + # Non-semantic versions + assert extract_ray_version_from_image("ray:latest") is None + assert extract_ray_version_from_image("ray:nightly") is None + assert ( + extract_ray_version_from_image("ray:v2.47") is None + ) # Missing patch version + + # Non-Ray images + assert extract_ray_version_from_image("python:3.11") is None + assert extract_ray_version_from_image("ubuntu:20.04") is None + + # Empty or None + assert extract_ray_version_from_image("") is None + assert extract_ray_version_from_image(None) is None + + def test_extract_ray_version_edge_cases(self): + """Test edge cases for version extraction.""" + # Version with 'v' prefix should not match our pattern + assert extract_ray_version_from_image("ray:v2.47.1") is None + + # Multiple version-like patterns - should match the first valid one + assert ( + extract_ray_version_from_image("registry/ray:2.47.1-based-on-1.0.0") + == "2.47.1" + ) + + +class TestRayVersionValidation: + """Test Ray version compatibility validation.""" + + def test_validate_compatible_versions(self): + """Test validation with compatible Ray versions.""" + # Exact match + is_compatible, is_warning, message = validate_ray_version_compatibility( + f"ray:{RAY_VERSION}" + ) + assert is_compatible is True + assert is_warning is False + assert "Ray versions match" in message + + # With registry and suffixes + is_compatible, is_warning, message = validate_ray_version_compatibility( + f"quay.io/modh/ray:{RAY_VERSION}-py311-cu121" + ) + assert is_compatible is True + assert is_warning is False + assert "Ray versions match" in message + + def test_validate_incompatible_versions(self): + """Test validation with incompatible Ray versions.""" + # Different version + is_compatible, is_warning, message = validate_ray_version_compatibility( + "ray:2.46.0" + ) + assert is_compatible is False + assert is_warning is False + assert "Ray version mismatch detected" in message + assert "CodeFlare SDK uses Ray" in message + assert "runtime image uses Ray" in message + + # Older version + is_compatible, is_warning, message = validate_ray_version_compatibility( + "ray:1.13.0" + ) + assert is_compatible is False + assert is_warning is False + assert "Ray version mismatch detected" in message + + def test_validate_empty_image(self): + """Test validation with no custom image (should use default).""" + # Empty string + is_compatible, is_warning, message = validate_ray_version_compatibility("") + assert is_compatible is True + assert is_warning is False + assert "Using default Ray image compatible with SDK" in message + + # None + is_compatible, is_warning, message = validate_ray_version_compatibility(None) + assert is_compatible is True + assert is_warning is False + assert "Using default Ray image compatible with SDK" in message + + def test_validate_unknown_version(self): + """Test validation when version cannot be determined.""" + # SHA-based image + is_compatible, is_warning, message = validate_ray_version_compatibility( + "quay.io/modh/ray@sha256:6d076aeb38ab3c34a6a2ef0f58dc667089aa15826fa08a73273c629333e12f1e" + ) + assert is_compatible is True + assert is_warning is True + assert "Cannot determine Ray version" in message + + # Custom image without version + is_compatible, is_warning, message = validate_ray_version_compatibility( + "my-custom-ray:latest" + ) + assert is_compatible is True + assert is_warning is True + assert "Cannot determine Ray version" in message + + def test_validate_custom_sdk_version(self): + """Test validation with custom SDK version.""" + # Compatible with custom SDK version + is_compatible, is_warning, message = validate_ray_version_compatibility( + "ray:2.46.0", "2.46.0" + ) + assert is_compatible is True + assert is_warning is False + assert "Ray versions match" in message + + # Incompatible with custom SDK version + is_compatible, is_warning, message = validate_ray_version_compatibility( + "ray:2.47.1", "2.46.0" + ) + assert is_compatible is False + assert is_warning is False + assert "CodeFlare SDK uses Ray 2.46.0" in message + assert "runtime image uses Ray 2.47.1" in message + + def test_validate_message_content(self): + """Test that validation messages contain expected guidance.""" + # Mismatch message should contain helpful guidance + is_compatible, is_warning, message = validate_ray_version_compatibility( + "ray:2.46.0" + ) + assert is_compatible is False + assert is_warning is False + assert "compatibility issues" in message.lower() + assert "unexpected behavior" in message.lower() + assert "please use a runtime image" in message.lower() + assert "update your sdk version" in message.lower() + + def test_semantic_version_comparison(self): + """Test that semantic version comparison works correctly.""" + # Test that 2.10.0 > 2.9.1 (would fail with string comparison) + is_compatible, is_warning, message = validate_ray_version_compatibility( + "ray:2.10.0", "2.9.1" + ) + assert is_compatible is False + assert is_warning is False + assert "CodeFlare SDK uses Ray 2.9.1" in message + assert "runtime image uses Ray 2.10.0" in message + + # Test that 2.9.1 < 2.10.0 (would fail with string comparison) + is_compatible, is_warning, message = validate_ray_version_compatibility( + "ray:2.9.1", "2.10.0" + ) + assert is_compatible is False + assert is_warning is False + assert "CodeFlare SDK uses Ray 2.10.0" in message + assert "runtime image uses Ray 2.9.1" in message diff --git a/src/codeflare_sdk/common/utils/unit_test_support.py b/src/codeflare_sdk/common/utils/unit_test_support.py index 9345fbc3..653e818c 100644 --- a/src/codeflare_sdk/common/utils/unit_test_support.py +++ b/src/codeflare_sdk/common/utils/unit_test_support.py @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import string +import sys +from codeflare_sdk.common.utils import constants +from codeflare_sdk.common.utils.utils import get_ray_image_for_python_version from codeflare_sdk.ray.cluster.cluster import ( Cluster, ClusterConfiguration, @@ -20,42 +24,45 @@ import yaml from pathlib import Path from kubernetes import client +from kubernetes.client import V1Toleration from unittest.mock import patch parent = Path(__file__).resolve().parents[4] # project directory aw_dir = os.path.expanduser("~/.codeflare/resources/") -def createClusterConfig(): +def create_cluster_config(num_workers=2, write_to_file=False): config = ClusterConfiguration( name="unit-test-cluster", namespace="ns", - num_workers=2, + num_workers=num_workers, worker_cpu_requests=3, worker_cpu_limits=4, worker_memory_requests=5, worker_memory_limits=6, appwrapper=True, - write_to_file=False, + write_to_file=write_to_file, ) return config -def createClusterWithConfig(mocker): - mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") - mocker.patch( - "kubernetes.client.CustomObjectsApi.get_cluster_custom_object", - return_value={"spec": {"domain": "apps.cluster.awsroute.org"}}, - ) - cluster = Cluster(createClusterConfig()) +def create_cluster(mocker, num_workers=2, write_to_file=False): + cluster = Cluster(create_cluster_config(num_workers, write_to_file)) return cluster -def createClusterWrongType(): +def patch_cluster_with_dynamic_client(mocker, cluster, dynamic_client=None): + mocker.patch.object(cluster, "get_dynamic_client", return_value=dynamic_client) + mocker.patch.object(cluster, "down", return_value=None) + mocker.patch.object(cluster, "config_check", return_value=None) + # mocker.patch.object(cluster, "_throw_for_no_raycluster", return_value=None) + + +def create_cluster_wrong_type(): config = ClusterConfiguration( name="unit-test-cluster", namespace="ns", - num_workers=2, + num_workers=True, worker_cpu_requests=[], worker_cpu_limits=4, worker_memory_requests=5, @@ -63,7 +70,7 @@ def createClusterWrongType(): worker_extended_resource_requests={"nvidia.com/gpu": 7}, appwrapper=True, image_pull_secrets=["unit-test-pull-secret"], - image="quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06", + image=constants.CUDA_PY312_RUNTIME_IMAGE, write_to_file=True, labels={1: 1}, ) @@ -143,9 +150,14 @@ def get_cluster_object(file_a, file_b): def get_ray_obj(group, version, namespace, plural): # To be used for mocking list_namespaced_custom_object for Ray Clusters - rc_a_path = f"{parent}/tests/test_cluster_yamls/support_clusters/test-rc-a.yaml" - rc_b_path = f"{parent}/tests/test_cluster_yamls/support_clusters/test-rc-b.yaml" - rc_a, rc_b = get_cluster_object(rc_a_path, rc_b_path) + rc_a = apply_template( + f"{parent}/tests/test_cluster_yamls/support_clusters/test-rc-a.yaml", + get_template_variables(), + ) + rc_b = apply_template( + f"{parent}/tests/test_cluster_yamls/support_clusters/test-rc-b.yaml", + get_template_variables(), + ) rc_list = {"items": [rc_a, rc_b]} return rc_list @@ -153,9 +165,14 @@ def get_ray_obj(group, version, namespace, plural): def get_ray_obj_with_status(group, version, namespace, plural): # To be used for mocking list_namespaced_custom_object for Ray Clusters with statuses - rc_a_path = f"{parent}/tests/test_cluster_yamls/support_clusters/test-rc-a.yaml" - rc_b_path = f"{parent}/tests/test_cluster_yamls/support_clusters/test-rc-b.yaml" - rc_a, rc_b = get_cluster_object(rc_a_path, rc_b_path) + rc_a = apply_template( + f"{parent}/tests/test_cluster_yamls/support_clusters/test-rc-a.yaml", + get_template_variables(), + ) + rc_b = apply_template( + f"{parent}/tests/test_cluster_yamls/support_clusters/test-rc-b.yaml", + get_template_variables(), + ) rc_a.update( { @@ -200,9 +217,14 @@ def get_ray_obj_with_status(group, version, namespace, plural): def get_aw_obj(group, version, namespace, plural): # To be used for mocking list_namespaced_custom_object for AppWrappers - aw_a_path = f"{parent}/tests/test_cluster_yamls/support_clusters/test-aw-a.yaml" - aw_b_path = f"{parent}/tests/test_cluster_yamls/support_clusters/test-aw-b.yaml" - aw_a, aw_b = get_cluster_object(aw_a_path, aw_b_path) + aw_a = apply_template( + f"{parent}/tests/test_cluster_yamls/support_clusters/test-aw-a.yaml", + get_template_variables(), + ) + aw_b = apply_template( + f"{parent}/tests/test_cluster_yamls/support_clusters/test-aw-b.yaml", + get_template_variables(), + ) aw_list = {"items": [aw_a, aw_b]} return aw_list @@ -210,9 +232,14 @@ def get_aw_obj(group, version, namespace, plural): def get_aw_obj_with_status(group, version, namespace, plural): # To be used for mocking list_namespaced_custom_object for AppWrappers with statuses - aw_a_path = f"{parent}/tests/test_cluster_yamls/support_clusters/test-aw-a.yaml" - aw_b_path = f"{parent}/tests/test_cluster_yamls/support_clusters/test-aw-b.yaml" - aw_a, aw_b = get_cluster_object(aw_a_path, aw_b_path) + aw_a = apply_template( + f"{parent}/tests/test_cluster_yamls/support_clusters/test-aw-a.yaml", + get_template_variables(), + ) + aw_b = apply_template( + f"{parent}/tests/test_cluster_yamls/support_clusters/test-aw-b.yaml", + get_template_variables(), + ) aw_a.update( { @@ -255,6 +282,29 @@ def arg_check_del_effect(group, version, namespace, plural, name, *args): assert name == "ray-dashboard-unit-test-cluster-ray" +def apply_template(yaml_file_path, variables): + with open(yaml_file_path, "r") as file: + yaml_content = file.read() + + # Create a Template instance and substitute the variables + template = string.Template(yaml_content) + filled_yaml = template.substitute(variables) + + # Now load the filled YAML into a Python object + return yaml.load(filled_yaml, Loader=yaml.FullLoader) + + +def get_expected_image(): + # Use centralized image selection logic (fallback to 3.12 for test consistency) + return get_ray_image_for_python_version(warn_on_unsupported=True) + + +def get_template_variables(): + return { + "image": get_expected_image(), + } + + def arg_check_apply_effect(group, version, namespace, plural, body, *args): assert namespace == "ns" assert args == tuple() @@ -383,12 +433,55 @@ def mocked_ingress(port, cluster_name="unit-test-cluster", annotations: dict = N return mock_ingress +# Global dictionary to maintain state in the mock +cluster_state = {} + + +# The mock side_effect function for server_side_apply +def mock_server_side_apply(resource, body=None, name=None, namespace=None, **kwargs): + # Simulate the behavior of server_side_apply: + # Update a mock state that represents the cluster's current configuration. + # Stores the state in a global dictionary for simplicity. + + global cluster_state + + if not resource or not body or not name or not namespace: + raise ValueError("Missing required parameters for server_side_apply") + + # Extract worker count from the body if it exists + try: + worker_count = ( + body["spec"]["workerGroupSpecs"][0]["replicas"] + if "spec" in body and "workerGroupSpecs" in body["spec"] + else None + ) + except KeyError: + worker_count = None + + # Apply changes to the cluster_state mock + cluster_state[name] = { + "namespace": namespace, + "worker_count": worker_count, + "body": body, + } + + # Return a response that mimics the behavior of a successful apply + return { + "status": "success", + "applied": True, + "name": name, + "namespace": namespace, + "worker_count": worker_count, + } + + @patch.dict("os.environ", {"NB_PREFIX": "test-prefix"}) def create_cluster_all_config_params(mocker, cluster_name, is_appwrapper) -> Cluster: mocker.patch( "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), ) + volumes, volume_mounts = get_example_extended_storage_opts() config = ClusterConfiguration( name=cluster_name, @@ -398,8 +491,18 @@ def create_cluster_all_config_params(mocker, cluster_name, is_appwrapper) -> Clu head_memory_requests=12, head_memory_limits=16, head_extended_resource_requests={"nvidia.com/gpu": 1, "intel.com/gpu": 2}, + head_tolerations=[ + V1Toleration( + key="key1", operator="Equal", value="value1", effect="NoSchedule" + ) + ], worker_cpu_requests=4, worker_cpu_limits=8, + worker_tolerations=[ + V1Toleration( + key="key2", operator="Equal", value="value2", effect="NoSchedule" + ) + ], num_workers=10, worker_memory_requests=12, worker_memory_limits=16, @@ -414,5 +517,50 @@ def create_cluster_all_config_params(mocker, cluster_name, is_appwrapper) -> Clu extended_resource_mapping={"example.com/gpu": "GPU", "intel.com/gpu": "TPU"}, overwrite_default_resource_mapping=True, local_queue="local-queue-default", + annotations={ + "key1": "value1", + "key2": "value2", + }, + volumes=volumes, + volume_mounts=volume_mounts, ) return Cluster(config) + + +def get_example_extended_storage_opts(): + from kubernetes.client import ( + V1Volume, + V1VolumeMount, + V1EmptyDirVolumeSource, + V1ConfigMapVolumeSource, + V1KeyToPath, + V1SecretVolumeSource, + ) + + volume_mounts = [ + V1VolumeMount(mount_path="/home/ray/test1", name="test"), + V1VolumeMount( + mount_path="/home/ray/test2", + name="test2", + ), + V1VolumeMount( + mount_path="/home/ray/test2", + name="test3", + ), + ] + + volumes = [ + V1Volume( + name="test", + empty_dir=V1EmptyDirVolumeSource(size_limit="500Gi"), + ), + V1Volume( + name="test2", + config_map=V1ConfigMapVolumeSource( + name="config-map-test", + items=[V1KeyToPath(key="test", path="/home/ray/test2/data.txt")], + ), + ), + V1Volume(name="test3", secret=V1SecretVolumeSource(secret_name="test-secret")), + ] + return volumes, volume_mounts diff --git a/src/codeflare_sdk/common/utils/utils.py b/src/codeflare_sdk/common/utils/utils.py new file mode 100644 index 00000000..7e30b994 --- /dev/null +++ b/src/codeflare_sdk/common/utils/utils.py @@ -0,0 +1,57 @@ +# Copyright 2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys + +from codeflare_sdk.common.utils.constants import ( + SUPPORTED_PYTHON_VERSIONS, + CUDA_PY312_RUNTIME_IMAGE, +) + + +def update_image(image) -> str: + """ + The update_image() function automatically sets the image config parameter to a preset image based on Python version if not specified. + This now points to the centralized function in utils.py. + """ + if not image: + # Pull the image based on the matching Python version (or output a warning if not supported) + image = get_ray_image_for_python_version(warn_on_unsupported=True) + return image + + +def get_ray_image_for_python_version(python_version=None, warn_on_unsupported=True): + """ + Get the appropriate Ray image for a given Python version. + If no version is provided, uses the current runtime Python version. + This prevents us needing to hard code image versions for tests. + + Args: + python_version: Python version string (e.g. "3.11"). If None, detects current version. + warn_on_unsupported: If True, warns and returns None for unsupported versions. + If False, silently falls back to Python 3.12 image. + """ + if python_version is None: + python_version = f"{sys.version_info.major}.{sys.version_info.minor}" + + if python_version in SUPPORTED_PYTHON_VERSIONS: + return SUPPORTED_PYTHON_VERSIONS[python_version] + elif warn_on_unsupported: + import warnings + + warnings.warn( + f"No default Ray image defined for {python_version}. Please provide your own image or use one of the following python versions: {', '.join(SUPPORTED_PYTHON_VERSIONS.keys())}." + ) + return None + else: + return CUDA_PY312_RUNTIME_IMAGE diff --git a/src/codeflare_sdk/common/utils/validation.py b/src/codeflare_sdk/common/utils/validation.py new file mode 100644 index 00000000..ec749f7c --- /dev/null +++ b/src/codeflare_sdk/common/utils/validation.py @@ -0,0 +1,134 @@ +# Copyright 2022-2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Validation utilities for the CodeFlare SDK. + +This module contains validation functions used across the SDK for ensuring +configuration compatibility and correctness. +""" + +import logging +import re +from typing import Optional, Tuple +from packaging.version import Version, InvalidVersion +from .constants import RAY_VERSION + +logger = logging.getLogger(__name__) + + +def extract_ray_version_from_image(image_name: str) -> Optional[str]: + """ + Extract Ray version from a container image name. + + Supports various image naming patterns: + - quay.io/modh/ray:2.47.1-py311-cu121 + - ray:2.47.1 + - some-registry/ray:2.47.1-py311 + - quay.io/modh/ray@sha256:... (falls back to None) + + Args: + image_name: The container image name/tag + + Returns: + The extracted Ray version, or None if not found + """ + if not image_name: + return None + + # Pattern to match semantic version after ray: or ray/ + # Looks for patterns like ray:2.47.1, ray:2.47.1-py311, etc. + patterns = [ + r"ray:(\d+\.\d+\.\d+)", # ray:2.47.1 + r"ray/[^:]*:(\d+\.\d+\.\d+)", # registry/ray:2.47.1 + r"/ray:(\d+\.\d+\.\d+)", # any-registry/ray:2.47.1 + ] + + for pattern in patterns: + match = re.search(pattern, image_name) + if match: + return match.group(1) + + # If we can't extract version, return None to indicate unknown + return None + + +def validate_ray_version_compatibility( + image_name: str, sdk_ray_version: str = RAY_VERSION +) -> Tuple[bool, bool, str]: + """ + Validate that the Ray version in the runtime image matches the SDK's Ray version. + + Args: + image_name: The container image name/tag + sdk_ray_version: The Ray version used by the CodeFlare SDK + + Returns: + tuple: (is_compatible, is_warning, message) + - is_compatible: True if versions match or cannot be determined, False if mismatch + - is_warning: True if this is a warning (non-fatal), False otherwise + - message: Descriptive message about the validation result + """ + if not image_name: + # No custom image specified, will use default - this is compatible + logger.debug("Using default Ray image compatible with SDK") + return True, False, "Using default Ray image compatible with SDK" + + image_ray_version = extract_ray_version_from_image(image_name) + + if image_ray_version is None: + # Cannot determine version from image name, issue a warning but allow + return ( + True, + True, + f"Cannot determine Ray version from image '{image_name}'. Please ensure it's compatible with Ray {sdk_ray_version}", + ) + + # Use semantic version comparison for robust version checking + try: + sdk_version = Version(sdk_ray_version) + image_version = Version(image_ray_version) + + if image_version != sdk_version: + # Version mismatch detected + message = ( + f"Ray version mismatch detected!\n" + f"CodeFlare SDK uses Ray {sdk_ray_version}, but runtime image uses Ray {image_ray_version}.\n" + f"This mismatch can cause compatibility issues and unexpected behavior.\n" + f"Please use a runtime image with Ray {sdk_ray_version} or update your SDK version." + ) + return False, False, message + except InvalidVersion as e: + # If version parsing fails, fall back to string comparison with a warning + logger.warning( + f"Failed to parse version for comparison ({e}), falling back to string comparison" + ) + if image_ray_version != sdk_ray_version: + message = ( + f"Ray version mismatch detected!\n" + f"CodeFlare SDK uses Ray {sdk_ray_version}, but runtime image uses Ray {image_ray_version}.\n" + f"This mismatch can cause compatibility issues and unexpected behavior.\n" + f"Please use a runtime image with Ray {sdk_ray_version} or update your SDK version." + ) + return False, False, message + + # Versions match + logger.debug( + f"Ray version validation successful: SDK and runtime image both use Ray {sdk_ray_version}" + ) + return ( + True, + False, + f"Ray versions match: SDK and runtime image both use Ray {sdk_ray_version}", + ) diff --git a/src/codeflare_sdk/common/widgets/test_widgets.py b/src/codeflare_sdk/common/widgets/test_widgets.py index 12c23854..55be2b75 100644 --- a/src/codeflare_sdk/common/widgets/test_widgets.py +++ b/src/codeflare_sdk/common/widgets/test_widgets.py @@ -15,7 +15,7 @@ import codeflare_sdk.common.widgets.widgets as cf_widgets import pandas as pd from unittest.mock import MagicMock, patch -from ..utils.unit_test_support import get_local_queue, createClusterConfig +from ..utils.unit_test_support import get_local_queue, create_cluster_config from codeflare_sdk.ray.cluster.cluster import Cluster from codeflare_sdk.ray.cluster.status import ( RayCluster, @@ -28,7 +28,7 @@ @patch.dict( "os.environ", {"JPY_SESSION_NAME": "example-test"} ) # Mock Jupyter environment variable -def test_cluster_up_down_buttons(mocker): +def test_cluster_apply_down_buttons(mocker): mocker.patch("kubernetes.client.ApisApi.get_api_versions") mocker.patch( "kubernetes.client.CustomObjectsApi.get_cluster_custom_object", @@ -38,43 +38,45 @@ def test_cluster_up_down_buttons(mocker): "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), ) - cluster = Cluster(createClusterConfig()) + cluster = Cluster(create_cluster_config()) with patch("ipywidgets.Button") as MockButton, patch( "ipywidgets.Checkbox" ) as MockCheckbox, patch("ipywidgets.Output"), patch("ipywidgets.HBox"), patch( "ipywidgets.VBox" ), patch.object( - cluster, "up" - ) as mock_up, patch.object( + cluster, "apply" + ) as mock_apply, patch.object( cluster, "down" ) as mock_down, patch.object( cluster, "wait_ready" ) as mock_wait_ready: # Create mock button & CheckBox instances - mock_up_button = MagicMock() + mock_apply_button = MagicMock() mock_down_button = MagicMock() mock_wait_ready_check_box = MagicMock() # Ensure the mock Button class returns the mock button instances in sequence MockCheckbox.side_effect = [mock_wait_ready_check_box] - MockButton.side_effect = [mock_up_button, mock_down_button] + MockButton.side_effect = [mock_apply_button, mock_down_button] # Call the method under test - cf_widgets.cluster_up_down_buttons(cluster) + cf_widgets.cluster_apply_down_buttons(cluster) # Simulate checkbox being checked or unchecked mock_wait_ready_check_box.value = True # Simulate checkbox being checked # Simulate the button clicks by calling the mock on_click handlers - mock_up_button.on_click.call_args[0][0](None) # Simulate clicking "Cluster Up" + mock_apply_button.on_click.call_args[0][0]( + None + ) # Simulate clicking "Cluster Apply" mock_down_button.on_click.call_args[0][0]( None ) # Simulate clicking "Cluster Down" - # Check if the `up` and `down` methods were called + # Check if the `apply` and `down` methods were called mock_wait_ready.assert_called_once() - mock_up.assert_called_once() + mock_apply.assert_called_once() mock_down.assert_called_once() @@ -104,7 +106,7 @@ def test_view_clusters(mocker, capsys): # Prepare to run view_clusters when notebook environment is detected mocker.patch("codeflare_sdk.common.widgets.widgets.is_notebook", return_value=True) mock_get_current_namespace = mocker.patch( - "codeflare_sdk.ray.cluster.cluster.get_current_namespace", + "codeflare_sdk.common.widgets.widgets.get_current_namespace", return_value="default", ) namespace = mock_get_current_namespace.return_value @@ -248,7 +250,7 @@ def test_ray_cluster_manager_widgets_init(mocker, capsys): return_value=test_ray_clusters_df, ) mocker.patch( - "codeflare_sdk.ray.cluster.cluster.get_current_namespace", + "codeflare_sdk.common.utils.get_current_namespace", return_value=namespace, ) mock_delete_cluster = mocker.patch( diff --git a/src/codeflare_sdk/common/widgets/widgets.py b/src/codeflare_sdk/common/widgets/widgets.py index 6f3283ce..c813fabc 100644 --- a/src/codeflare_sdk/common/widgets/widgets.py +++ b/src/codeflare_sdk/common/widgets/widgets.py @@ -26,6 +26,8 @@ import ipywidgets as widgets from IPython.display import display, HTML, Javascript import pandas as pd + +from ...common.utils import get_current_namespace from ...ray.cluster.config import ClusterConfiguration from ...ray.cluster.status import RayClusterStatus from ..kubernetes_cluster import _kube_api_error_handling @@ -43,8 +45,6 @@ class RayClusterManagerWidgets: """ def __init__(self, ray_clusters_df: pd.DataFrame, namespace: str = None): - from ...ray.cluster.cluster import get_current_namespace - # Data self.ray_clusters_df = ray_clusters_df self.namespace = get_current_namespace() if not namespace else namespace @@ -271,19 +271,19 @@ def display_widgets(self): ) -def cluster_up_down_buttons( +def cluster_apply_down_buttons( cluster: "codeflare_sdk.ray.cluster.cluster.Cluster", ) -> widgets.Button: """ - The cluster_up_down_buttons function returns two button widgets for a create and delete button. + The cluster_apply_down_buttons function returns two button widgets for a create and delete button. The function uses the appwrapper bool to distinguish between resource type for the tool tip. """ resource = "Ray Cluster" if cluster.config.appwrapper: resource = "AppWrapper" - up_button = widgets.Button( - description="Cluster Up", + apply_button = widgets.Button( + description="Cluster Apply", tooltip=f"Create the {resource}", icon="play", ) @@ -298,13 +298,13 @@ def cluster_up_down_buttons( output = widgets.Output() # Display the buttons in an HBox wrapped in a VBox which includes the wait_ready Checkbox - button_display = widgets.HBox([up_button, delete_button]) + button_display = widgets.HBox([apply_button, delete_button]) display(widgets.VBox([button_display, wait_ready_check]), output) - def on_up_button_clicked(b): # Handle the up button click event + def on_apply_button_clicked(b): # Handle the apply button click event with output: output.clear_output() - cluster.up() + cluster.apply() # If the wait_ready Checkbox is clicked(value == True) trigger the wait_ready function if wait_ready_check.value: @@ -315,7 +315,7 @@ def on_down_button_clicked(b): # Handle the down button click event output.clear_output() cluster.down() - up_button.on_click(on_up_button_clicked) + apply_button.on_click(on_apply_button_clicked) delete_button.on_click(on_down_button_clicked) @@ -353,8 +353,6 @@ def view_clusters(namespace: str = None): ) return # Exit function if not in Jupyter Notebook - from ...ray.cluster.cluster import get_current_namespace - if not namespace: namespace = get_current_namespace() diff --git a/src/codeflare_sdk/ray/__init__.py b/src/codeflare_sdk/ray/__init__.py index ab55cc82..7bd0b2c8 100644 --- a/src/codeflare_sdk/ray/__init__.py +++ b/src/codeflare_sdk/ray/__init__.py @@ -4,6 +4,14 @@ RayJobClient, ) +from .rayjobs import ( + RayJob, + ManagedClusterConfig, + RayJobDeploymentStatus, + CodeflareRayJobStatus, + RayJobInfo, +) + from .cluster import ( Cluster, ClusterConfiguration, diff --git a/src/codeflare_sdk/ray/appwrapper/test_awload.py b/src/codeflare_sdk/ray/appwrapper/test_awload.py index 6909394b..3f45e1a5 100644 --- a/src/codeflare_sdk/ray/appwrapper/test_awload.py +++ b/src/codeflare_sdk/ray/appwrapper/test_awload.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. from codeflare_sdk.common.utils.unit_test_support import ( + apply_template, arg_check_aw_apply_effect, arg_check_aw_del_effect, + get_template_variables, ) from codeflare_sdk.ray.appwrapper import AWManager from codeflare_sdk.ray.cluster import Cluster, ClusterConfiguration @@ -47,8 +49,11 @@ def test_AWManager_creation(mocker): assert type(e) == FileNotFoundError assert str(e) == "[Errno 2] No such file or directory: 'fake'" try: - testaw = AWManager( - f"{parent}/tests/test_cluster_yamls/appwrapper/test-case-bad.yaml" + testaw = apply_template( + AWManager( + f"{parent}/tests/test_cluster_yamls/appwrapper/test-case-bad.yaml" + ), + get_template_variables(), ) except Exception as e: assert type(e) == ValueError diff --git a/src/codeflare_sdk/ray/cluster/build_ray_cluster.py b/src/codeflare_sdk/ray/cluster/build_ray_cluster.py index e590d483..6a3984b1 100644 --- a/src/codeflare_sdk/ray/cluster/build_ray_cluster.py +++ b/src/codeflare_sdk/ray/cluster/build_ray_cluster.py @@ -16,10 +16,12 @@ This sub-module exists primarily to be used internally by the Cluster object (in the cluster sub-module) for RayCluster/AppWrapper generation. """ -from typing import Union, Tuple, Dict +from typing import List, Union, Tuple, Dict from ...common import _kube_api_error_handling from ...common.kubernetes_cluster import get_api_client, config_check from kubernetes.client.exceptions import ApiException +from ...common.utils.constants import RAY_VERSION +from ...common.utils.utils import update_image import codeflare_sdk import os @@ -40,6 +42,7 @@ V1PodTemplateSpec, V1PodSpec, V1LocalObjectReference, + V1Toleration, ) import yaml @@ -48,6 +51,8 @@ import warnings import json +from codeflare_sdk.common.utils import constants + FORBIDDEN_CUSTOM_RESOURCE_TYPES = ["GPU", "CPU", "memory"] VOLUME_MOUNTS = [ V1VolumeMount( @@ -91,11 +96,6 @@ ), ] -SUPPORTED_PYTHON_VERSIONS = { - "3.9": "quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06", - "3.11": "quay.io/modh/ray@sha256:db667df1bc437a7b0965e8031e905d3ab04b86390d764d120e05ea5a5c18d1b4", -} - # RayCluster/AppWrapper builder function def build_ray_cluster(cluster: "codeflare_sdk.ray.cluster.Cluster"): @@ -104,7 +104,6 @@ def build_ray_cluster(cluster: "codeflare_sdk.ray.cluster.Cluster"): The resource is a dict template which uses Kubernetes Objects for creating metadata, resource requests, specs and containers. The result is sanitised and returned either as a dict or written as a yaml file. """ - ray_version = "2.35.0" # GPU related variables head_gpu_count, worker_gpu_count = head_worker_gpu_count_from_cluster(cluster) @@ -122,7 +121,7 @@ def build_ray_cluster(cluster: "codeflare_sdk.ray.cluster.Cluster"): "kind": "RayCluster", "metadata": get_metadata(cluster), "spec": { - "rayVersion": ray_version, + "rayVersion": RAY_VERSION, "enableInTreeAutoscaling": False, "autoscalerOptions": { "upscalingMode": "Default", @@ -138,9 +137,16 @@ def build_ray_cluster(cluster: "codeflare_sdk.ray.cluster.Cluster"): "num-gpus": str(head_gpu_count), "resources": head_resources, }, - "template": { - "spec": get_pod_spec(cluster, [get_head_container_spec(cluster)]) - }, + "template": V1PodTemplateSpec( + metadata=V1ObjectMeta(cluster.config.annotations) + if cluster.config.annotations + else None, + spec=get_pod_spec( + cluster, + [get_head_container_spec(cluster)], + cluster.config.head_tolerations, + ), + ), }, "workerGroupSpecs": [ { @@ -154,13 +160,45 @@ def build_ray_cluster(cluster: "codeflare_sdk.ray.cluster.Cluster"): "resources": worker_resources, }, "template": V1PodTemplateSpec( - spec=get_pod_spec(cluster, [get_worker_container_spec(cluster)]) + metadata=V1ObjectMeta(cluster.config.annotations) + if cluster.config.annotations + else None, + spec=get_pod_spec( + cluster, + [get_worker_container_spec(cluster)], + cluster.config.worker_tolerations, + ), ), } ], }, } + if cluster.config.enable_gcs_ft: + if not cluster.config.redis_address: + raise ValueError( + "redis_address must be provided when enable_gcs_ft is True" + ) + + gcs_ft_options = {"redisAddress": cluster.config.redis_address} + + if cluster.config.external_storage_namespace: + gcs_ft_options[ + "externalStorageNamespace" + ] = cluster.config.external_storage_namespace + + if cluster.config.redis_password_secret: + gcs_ft_options["redisPassword"] = { + "valueFrom": { + "secretKeyRef": { + "name": cluster.config.redis_password_secret["name"], + "key": cluster.config.redis_password_secret["key"], + } + } + } + + resource["spec"]["gcsFaultToleranceOptions"] = gcs_ft_options + config_check() k8s_client = get_api_client() or client.ApiClient() @@ -182,7 +220,7 @@ def build_ray_cluster(cluster: "codeflare_sdk.ray.cluster.Cluster"): # Metadata related functions def get_metadata(cluster: "codeflare_sdk.ray.cluster.Cluster"): """ - The get_metadata() function builds and returns a V1ObjectMeta Object using cluster configurtation parameters + The get_metadata() function builds and returns a V1ObjectMeta Object using cluster configuration parameters """ object_meta = V1ObjectMeta( name=cluster.config.name, @@ -191,9 +229,10 @@ def get_metadata(cluster: "codeflare_sdk.ray.cluster.Cluster"): ) # Get the NB annotation if it exists - could be useful in future for a "annotations" parameter. - annotations = get_nb_annotations() + annotations = with_nb_annotations(cluster.config.annotations) if annotations != {}: object_meta.annotations = annotations # As annotations are not a guarantee they are appended to the metadata after creation. + return object_meta @@ -203,6 +242,7 @@ def get_labels(cluster: "codeflare_sdk.ray.cluster.Cluster"): """ labels = { "controller-tools.k8s.io": "1.0", + "ray.io/cluster": cluster.config.name, # Enforced label always present } if cluster.config.labels != {}: labels.update(cluster.config.labels) @@ -213,11 +253,10 @@ def get_labels(cluster: "codeflare_sdk.ray.cluster.Cluster"): return labels -def get_nb_annotations(): +def with_nb_annotations(annotations: dict): """ - The get_nb_annotations() function generates the annotation for NB Prefix if the SDK is running in a notebook + The with_nb_annotations() function generates the annotation for NB Prefix if the SDK is running in a notebook and appends any user set annotations """ - annotations = {} # Notebook annotation nb_prefix = os.environ.get("NB_PREFIX") @@ -228,30 +267,21 @@ def get_nb_annotations(): # Head/Worker container related functions -def update_image(image) -> str: - """ - The update_image() function automatically sets the image config parameter to a preset image based on Python version if not specified. - If no Ray image exists for the given Python version a warning is produced. - """ - if not image: - python_version = f"{sys.version_info.major}.{sys.version_info.minor}" - if python_version in SUPPORTED_PYTHON_VERSIONS: - image = SUPPORTED_PYTHON_VERSIONS[python_version] - else: - warnings.warn( - f"No default Ray image defined for {python_version}. Please provide your own image or use one of the following python versions: {', '.join(SUPPORTED_PYTHON_VERSIONS.keys())}." - ) - return image - - -def get_pod_spec(cluster: "codeflare_sdk.ray.cluster.Cluster", containers): +def get_pod_spec( + cluster: "codeflare_sdk.ray.cluster.Cluster", + containers: List, + tolerations: List[V1Toleration], +) -> V1PodSpec: """ The get_pod_spec() function generates a V1PodSpec for the head/worker containers """ + pod_spec = V1PodSpec( containers=containers, - volumes=VOLUMES, + volumes=generate_custom_storage(cluster.config.volumes, VOLUMES), + tolerations=tolerations or None, ) + if cluster.config.image_pull_secrets != []: pod_spec.image_pull_secrets = generate_image_pull_secrets(cluster) @@ -296,7 +326,9 @@ def get_head_container_spec( cluster.config.head_memory_limits, cluster.config.head_extended_resource_requests, ), - volume_mounts=VOLUME_MOUNTS, + volume_mounts=generate_custom_storage( + cluster.config.volume_mounts, VOLUME_MOUNTS + ), ) if cluster.config.envs != {}: head_container.env = generate_env_vars(cluster) @@ -338,7 +370,9 @@ def get_worker_container_spec( cluster.config.worker_memory_limits, cluster.config.worker_extended_resource_requests, ), - volume_mounts=VOLUME_MOUNTS, + volume_mounts=generate_custom_storage( + cluster.config.volume_mounts, VOLUME_MOUNTS + ), ) if cluster.config.envs != {}: @@ -438,9 +472,11 @@ def add_queue_label(cluster: "codeflare_sdk.ray.cluster.Cluster", labels: dict): if lq_name == None: return elif not local_queue_exists(cluster): - raise ValueError( + # ValueError removed to pass validation to validating admission policy + print( "local_queue provided does not exist or is not in this namespace. Please provide the correct local_queue name in Cluster Configuration" ) + return labels.update({"kueue.x-k8s.io/queue-name": lq_name}) @@ -522,6 +558,22 @@ def wrap_cluster( # Etc. +def generate_custom_storage(provided_storage: list, default_storage: list): + """ + The generate_custom_storage function updates the volumes/volume mounts configs with the default volumes/volume mounts. + """ + storage_list = provided_storage.copy() + + if storage_list == []: + storage_list = default_storage + else: + # We append the list of volumes/volume mounts with the defaults and return the full list + for storage in default_storage: + storage_list.append(storage) + + return storage_list + + def write_to_file(cluster: "codeflare_sdk.ray.cluster.Cluster", resource: dict): """ The write_to_file function writes the built Ray Cluster/AppWrapper dict as a yaml file in the .codeflare folder diff --git a/src/codeflare_sdk/ray/cluster/cluster.py b/src/codeflare_sdk/ray/cluster/cluster.py index a3f34554..8538dba3 100644 --- a/src/codeflare_sdk/ray/cluster/cluster.py +++ b/src/codeflare_sdk/ray/cluster/cluster.py @@ -20,8 +20,14 @@ from time import sleep from typing import List, Optional, Tuple, Dict +import copy -from ray.job_submission import JobSubmissionClient +from ray.job_submission import JobSubmissionClient, JobStatus +import time +import uuid +import warnings + +from ...common.utils import get_current_namespace from ...common.kubernetes_cluster.auth import ( config_check, @@ -43,7 +49,7 @@ AppWrapperStatus, ) from ...common.widgets.widgets import ( - cluster_up_down_buttons, + cluster_apply_down_buttons, is_notebook, ) from kubernetes import client @@ -52,8 +58,13 @@ import requests from kubernetes import config +from kubernetes.dynamic import DynamicClient +from kubernetes import client as k8s_client from kubernetes.client.rest import ApiException -import warnings + +from kubernetes.client.rest import ApiException + +CF_SDK_FIELD_MANAGER = "codeflare-sdk" class Cluster: @@ -82,7 +93,13 @@ def __init__(self, config: ClusterConfiguration): self.resource_yaml = self.create_resource() if is_notebook(): - cluster_up_down_buttons(self) + cluster_apply_down_buttons(self) + + def get_dynamic_client(self): # pragma: no cover + return DynamicClient(get_api_client()) + + def config_check(self): + return config_check() @property def _client_headers(self): @@ -95,9 +112,7 @@ def _client_headers(self): @property def _client_verify_tls(self): - if not _is_openshift_cluster or not self.config.verify_tls: - return False - return True + return _is_openshift_cluster and self.config.verify_tls @property def job_client(self): @@ -121,7 +136,6 @@ def create_resource(self): Called upon cluster object creation, creates an AppWrapper yaml based on the specifications of the ClusterConfiguration. """ - if self.config.namespace is None: self.config.namespace = get_current_namespace() if self.config.namespace is None: @@ -130,7 +144,6 @@ def create_resource(self): raise TypeError( f"Namespace {self.config.namespace} is of type {type(self.config.namespace)}. Check your Kubernetes Authentication." ) - return build_ray_cluster(self) # creates a new cluster with the provided or default spec @@ -139,10 +152,11 @@ def up(self): Applies the Cluster yaml, pushing the resource request onto the Kueue localqueue. """ - + print( + "WARNING: The up() function is planned for deprecation in favor of apply()." + ) # check if RayCluster CustomResourceDefinition exists if not throw RuntimeError self._throw_for_no_raycluster() - namespace = self.config.namespace try: @@ -174,6 +188,78 @@ def up(self): f"Ray Cluster: '{self.config.name}' has successfully been created" ) except Exception as e: # pragma: no cover + if e.status == 422: + print( + "WARNING: RayCluster creation rejected due to invalid Kueue configuration. Please contact your administrator." + ) + else: + print( + "WARNING: Failed to create RayCluster due to unexpected error. Please contact your administrator." + ) + return _kube_api_error_handling(e) + + # Applies a new cluster with the provided or default spec + def apply(self, force=False): + """ + Applies the Cluster yaml using server-side apply. + If 'force' is set to True, conflicts will be forced. + """ + # check if RayCluster CustomResourceDefinition exists if not throw RuntimeError + self._throw_for_no_raycluster() + namespace = self.config.namespace + name = self.config.name + + # Regenerate resource_yaml to reflect any configuration changes + self.resource_yaml = self.create_resource() + + try: + self.config_check() + api_instance = client.CustomObjectsApi(get_api_client()) + crds = self.get_dynamic_client().resources + if self.config.appwrapper: + api_version = "workload.codeflare.dev/v1beta2" + api_instance = crds.get(api_version=api_version, kind="AppWrapper") + # defaulting body to resource_yaml + body = self.resource_yaml + if self.config.write_to_file: + # if write_to_file is True, load the file from AppWrapper yaml and update body + with open(self.resource_yaml) as f: + aw = yaml.load(f, Loader=yaml.FullLoader) + body = aw + api_instance.server_side_apply( + field_manager=CF_SDK_FIELD_MANAGER, + group="workload.codeflare.dev", + version="v1beta2", + namespace=namespace, + plural="appwrappers", + body=body, + force_conflicts=force, + ) + print( + f"AppWrapper: '{name}' configuration has successfully been applied. For optimal resource management, you should delete this Ray Cluster when no longer in use." + ) + else: + api_version = "ray.io/v1" + api_instance = crds.get(api_version=api_version, kind="RayCluster") + self._component_resources_apply( + namespace=namespace, api_instance=api_instance + ) + print( + f"Ray Cluster: '{name}' has successfully been applied. For optimal resource management, you should delete this Ray Cluster when no longer in use." + ) + except AttributeError as e: + raise RuntimeError(f"Failed to initialize DynamicClient: {e}") + except Exception as e: # pragma: no cover + if ( + hasattr(e, "status") and e.status == 422 + ): # adding status check to avoid returning false positive + print( + "WARNING: RayCluster creation rejected due to invalid Kueue configuration. Please contact your administrator." + ) + else: + print( + "WARNING: Failed to create RayCluster due to unexpected error. Please contact your administrator." + ) return _kube_api_error_handling(e) def _throw_for_no_raycluster(self): @@ -204,7 +290,7 @@ def down(self): resource_name = self.config.name self._throw_for_no_raycluster() try: - config_check() + self.config_check() api_instance = client.CustomObjectsApi(get_api_client()) if self.config.appwrapper: api_instance.delete_namespaced_custom_object( @@ -307,9 +393,14 @@ def is_dashboard_ready(self) -> bool: bool: True if the dashboard is ready, False otherwise. """ + + dashboard_uri = self.cluster_dashboard_uri() + if dashboard_uri is None: + return False + try: response = requests.get( - self.cluster_dashboard_uri(), + dashboard_uri, headers=self._client_headers, timeout=5, verify=self._client_verify_tls, @@ -317,6 +408,10 @@ def is_dashboard_ready(self) -> bool: except requests.exceptions.SSLError: # pragma no cover # SSL exception occurs when oauth ingress has been created but cluster is not up return False + except Exception: # pragma no cover + # Any other exception (connection errors, timeouts, etc.) + return False + if response.status_code == 200: return True else: @@ -351,7 +446,7 @@ def wait_ready(self, timeout: Optional[int] = None, dashboard_check: bool = True status, ready = self.status(print_to_console=False) if status == CodeFlareClusterStatus.UNKNOWN: print( - "WARNING: Current cluster status is unknown, have you run cluster.up yet?" + "WARNING: Current cluster status is unknown, have you run cluster.apply() yet? Run cluster.details() to check if it's ready." ) if ready: break @@ -400,8 +495,19 @@ def cluster_uri(self) -> str: def cluster_dashboard_uri(self) -> str: """ Returns a string containing the cluster's dashboard URI. + Tries HTTPRoute first (RHOAI v3.0+), then falls back to OpenShift Routes or Ingresses. """ config_check() + + # Try HTTPRoute first (RHOAI v3.0+) + # This will return None if HTTPRoute is not found (SDK v0.31.1 and below or Kind clusters) + httproute_url = _get_dashboard_url_from_httproute( + self.config.name, self.config.namespace + ) + if httproute_url: + return httproute_url + + # Fall back to OpenShift Routes (pre-v3.0) or Ingresses (Kind) if _is_openshift_cluster(): try: api_instance = client.CustomObjectsApi(get_api_client()) @@ -424,6 +530,8 @@ def cluster_dashboard_uri(self) -> str: ): protocol = "https" if route["spec"].get("tls") else "http" return f"{protocol}://{route['spec']['host']}" + # No route found for this cluster + return "Dashboard not available yet, have you run cluster.apply()?" else: try: api_instance = client.NetworkingV1Api(get_api_client()) @@ -443,7 +551,7 @@ def cluster_dashboard_uri(self) -> str: elif "route.openshift.io/termination" in annotations: protocol = "https" return f"{protocol}://{ingress.spec.rules[0].host}" - return "Dashboard not available yet, have you run cluster.up()?" + return "Dashboard not available yet, have you run cluster.apply()? Run cluster.details() to check if it's ready." def list_jobs(self) -> List: """ @@ -507,6 +615,16 @@ def _component_resources_up( else: _create_resources(self.resource_yaml, namespace, api_instance) + def _component_resources_apply( + self, namespace: str, api_instance: client.CustomObjectsApi + ): + if self.config.write_to_file: + with open(self.resource_yaml) as f: + ray_cluster = yaml.safe_load(f) + _apply_ray_cluster(ray_cluster, namespace, api_instance) + else: + _apply_ray_cluster(self.resource_yaml, namespace, api_instance) + def _component_resources_down( self, namespace: str, api_instance: client.CustomObjectsApi ): @@ -550,32 +668,6 @@ def list_all_queued( return resources -def get_current_namespace(): # pragma: no cover - """ - Retrieves the current Kubernetes namespace. - - Returns: - str: - The current namespace or None if not found. - """ - if os.path.isfile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"): - try: - file = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") - active_context = file.readline().strip("\n") - return active_context - except Exception as e: - print("Unable to find current namespace") - print("trying to gather from current context") - try: - _, active_context = config.list_kube_config_contexts(config_check()) - except Exception as e: - return _kube_api_error_handling(e) - try: - return active_context["context"]["namespace"] - except KeyError: - return None - - def get_cluster( cluster_name: str, namespace: str = "default", @@ -675,6 +767,7 @@ def get_cluster( head_extended_resource_requests=head_extended_resources, worker_extended_resource_requests=worker_extended_resources, ) + # Ignore the warning here for the lack of a ClusterConfiguration with warnings.catch_warnings(): warnings.filterwarnings( @@ -718,6 +811,7 @@ def remove_autogenerated_fields(resource): del resource[key] else: remove_autogenerated_fields(resource[key]) + elif isinstance(resource, list): for item in resource: remove_autogenerated_fields(item) @@ -744,6 +838,20 @@ def _create_resources(yamls, namespace: str, api_instance: client.CustomObjectsA ) +def _apply_ray_cluster( + yamls, namespace: str, api_instance: client.CustomObjectsApi, force=False +): + api_instance.server_side_apply( + field_manager=CF_SDK_FIELD_MANAGER, + group="ray.io", + version="v1", + namespace=namespace, + plural="rayclusters", + body=yamls, + force_conflicts=force, # Allow forcing conflicts if needed + ) + + def _check_aw_exists(name: str, namespace: str) -> bool: try: config_check() @@ -904,45 +1012,51 @@ def _map_to_ray_cluster(rc) -> Optional[RayCluster]: status = RayClusterStatus.UNKNOWN config_check() dashboard_url = None - if _is_openshift_cluster(): - try: - api_instance = client.CustomObjectsApi(get_api_client()) - routes = api_instance.list_namespaced_custom_object( - group="route.openshift.io", - version="v1", - namespace=rc["metadata"]["namespace"], - plural="routes", - ) - except Exception as e: # pragma: no cover - return _kube_api_error_handling(e) - for route in routes["items"]: - rc_name = rc["metadata"]["name"] - if route["metadata"]["name"] == f"ray-dashboard-{rc_name}" or route[ - "metadata" - ]["name"].startswith(f"{rc_name}-ingress"): - protocol = "https" if route["spec"].get("tls") else "http" - dashboard_url = f"{protocol}://{route['spec']['host']}" - else: - try: - api_instance = client.NetworkingV1Api(get_api_client()) - ingresses = api_instance.list_namespaced_ingress( - rc["metadata"]["namespace"] - ) - except Exception as e: # pragma no cover - return _kube_api_error_handling(e) - for ingress in ingresses.items: - annotations = ingress.metadata.annotations - protocol = "http" - if ( - ingress.metadata.name == f"ray-dashboard-{rc['metadata']['name']}" - or ingress.metadata.name.startswith(f"{rc['metadata']['name']}-ingress") - ): - if annotations == None: - protocol = "http" - elif "route.openshift.io/termination" in annotations: - protocol = "https" - dashboard_url = f"{protocol}://{ingress.spec.rules[0].host}" + # Try HTTPRoute first (RHOAI v3.0+) + rc_name = rc["metadata"]["name"] + rc_namespace = rc["metadata"]["namespace"] + dashboard_url = _get_dashboard_url_from_httproute(rc_name, rc_namespace) + + # Fall back to OpenShift Routes or Ingresses if HTTPRoute not found + if not dashboard_url: + if _is_openshift_cluster(): + try: + api_instance = client.CustomObjectsApi(get_api_client()) + routes = api_instance.list_namespaced_custom_object( + group="route.openshift.io", + version="v1", + namespace=rc_namespace, + plural="routes", + ) + except Exception as e: # pragma: no cover + return _kube_api_error_handling(e) + + for route in routes["items"]: + if route["metadata"]["name"] == f"ray-dashboard-{rc_name}" or route[ + "metadata" + ]["name"].startswith(f"{rc_name}-ingress"): + protocol = "https" if route["spec"].get("tls") else "http" + dashboard_url = f"{protocol}://{route['spec']['host']}" + break + else: + try: + api_instance = client.NetworkingV1Api(get_api_client()) + ingresses = api_instance.list_namespaced_ingress(rc_namespace) + except Exception as e: # pragma no cover + return _kube_api_error_handling(e) + for ingress in ingresses.items: + annotations = ingress.metadata.annotations + protocol = "http" + if ( + ingress.metadata.name == f"ray-dashboard-{rc_name}" + or ingress.metadata.name.startswith(f"{rc_name}-ingress") + ): + if annotations == None: + protocol = "http" + elif "route.openshift.io/termination" in annotations: + protocol = "https" + dashboard_url = f"{protocol}://{ingress.spec.rules[0].host}" ( head_extended_resources, @@ -1032,3 +1146,80 @@ def _is_openshift_cluster(): return False except Exception as e: # pragma: no cover return _kube_api_error_handling(e) + + +# Get dashboard URL from HTTPRoute (RHOAI v3.0+) +def _get_dashboard_url_from_httproute( + cluster_name: str, namespace: str +) -> Optional[str]: + """ + Attempts to get the Ray dashboard URL from an HTTPRoute resource. + This is used for RHOAI v3.0+ clusters that use Gateway API. + + Args: + cluster_name: Name of the Ray cluster + namespace: Namespace of the Ray cluster + + Returns: + Dashboard URL if HTTPRoute is found, None otherwise + """ + try: + config_check() + api_instance = client.CustomObjectsApi(get_api_client()) + + # Try to get HTTPRoute for this Ray cluster + try: + httproute = api_instance.get_namespaced_custom_object( + group="gateway.networking.k8s.io", + version="v1", + namespace=namespace, + plural="httproutes", + name=cluster_name, + ) + except client.exceptions.ApiException as e: + if e.status == 404: + # HTTPRoute not found - this is expected for SDK v0.31.1 and below or Kind clusters + return None + raise + + # Get the Gateway reference from HTTPRoute + parent_refs = httproute.get("spec", {}).get("parentRefs", []) + if not parent_refs: + return None + + gateway_ref = parent_refs[0] + gateway_name = gateway_ref.get("name") + gateway_namespace = gateway_ref.get("namespace") + + if not gateway_name or not gateway_namespace: + return None + + # Get the Gateway to retrieve the hostname + gateway = api_instance.get_namespaced_custom_object( + group="gateway.networking.k8s.io", + version="v1", + namespace=gateway_namespace, + plural="gateways", + name=gateway_name, + ) + + # Extract hostname from Gateway listeners + listeners = gateway.get("spec", {}).get("listeners", []) + if not listeners: + return None + + hostname = listeners[0].get("hostname") + if not hostname: + return None + + # Construct the dashboard URL using RHOAI v3.0+ Gateway API pattern + # The HTTPRoute existence confirms v3.0+, so we use the standard path pattern + # Format: https://{hostname}/ray/{namespace}/{cluster-name} + protocol = "https" # Gateway API uses HTTPS + dashboard_url = f"{protocol}://{hostname}/ray/{namespace}/{cluster_name}" + + return dashboard_url + + except Exception as e: # pragma: no cover + # If any error occurs, return None to fall back to OpenShift Route + return None diff --git a/src/codeflare_sdk/ray/cluster/config.py b/src/codeflare_sdk/ray/cluster/config.py index f321c278..dc61de2a 100644 --- a/src/codeflare_sdk/ray/cluster/config.py +++ b/src/codeflare_sdk/ray/cluster/config.py @@ -22,6 +22,7 @@ import warnings from dataclasses import dataclass, field, fields from typing import Dict, List, Optional, Union, get_args, get_origin +from kubernetes.client import V1Toleration, V1Volume, V1VolumeMount dir = pathlib.Path(__file__).parent.parent.resolve() @@ -49,26 +50,14 @@ class ClusterConfiguration: The name of the cluster. namespace: The namespace in which the cluster should be created. - head_cpus: - The number of CPUs to allocate to the head node. - head_memory: - The amount of memory to allocate to the head node. - head_gpus: - The number of GPUs to allocate to the head node. (Deprecated, use head_extended_resource_requests) head_extended_resource_requests: A dictionary of extended resource requests for the head node. ex: {"nvidia.com/gpu": 1} - min_cpus: - The minimum number of CPUs to allocate to each worker. - max_cpus: - The maximum number of CPUs to allocate to each worker. + head_tolerations: + List of tolerations for head nodes. num_workers: The number of workers to create. - min_memory: - The minimum amount of memory to allocate to each worker. - max_memory: - The maximum amount of memory to allocate to each worker. - num_gpus: - The number of GPUs to allocate to each worker. (Deprecated, use worker_extended_resource_requests) + worker_tolerations: + List of tolerations for worker nodes. appwrapper: A boolean indicating whether to use an AppWrapper. envs: @@ -89,30 +78,40 @@ class ClusterConfiguration: A dictionary of custom resource mappings to map extended resource requests to RayCluster resource names overwrite_default_resource_mapping: A boolean indicating whether to overwrite the default resource mapping. + annotations: + A dictionary of annotations to apply to the cluster. + volumes: + A list of V1Volume objects to add to the Cluster + volume_mounts: + A list of V1VolumeMount objects to add to the Cluster + enable_gcs_ft: + A boolean indicating whether to enable GCS fault tolerance. + enable_usage_stats: + A boolean indicating whether to capture and send Ray usage stats externally. + redis_address: + The address of the Redis server to use for GCS fault tolerance, required when enable_gcs_ft is True. + redis_password_secret: + Kubernetes secret reference containing Redis password. ex: {"name": "secret-name", "key": "password-key"} + external_storage_namespace: + The storage namespace to use for GCS fault tolerance. By default, KubeRay sets it to the UID of RayCluster. """ name: str namespace: Optional[str] = None head_cpu_requests: Union[int, str] = 2 head_cpu_limits: Union[int, str] = 2 - head_cpus: Optional[Union[int, str]] = None # Deprecating head_memory_requests: Union[int, str] = 8 head_memory_limits: Union[int, str] = 8 - head_memory: Optional[Union[int, str]] = None # Deprecating - head_gpus: Optional[int] = None # Deprecating head_extended_resource_requests: Dict[str, Union[str, int]] = field( default_factory=dict ) + head_tolerations: Optional[List[V1Toleration]] = None worker_cpu_requests: Union[int, str] = 1 worker_cpu_limits: Union[int, str] = 1 - min_cpus: Optional[Union[int, str]] = None # Deprecating - max_cpus: Optional[Union[int, str]] = None # Deprecating num_workers: int = 1 worker_memory_requests: Union[int, str] = 2 worker_memory_limits: Union[int, str] = 2 - min_memory: Optional[Union[int, str]] = None # Deprecating - max_memory: Optional[Union[int, str]] = None # Deprecating - num_gpus: Optional[int] = None # Deprecating + worker_tolerations: Optional[List[V1Toleration]] = None appwrapper: bool = False envs: Dict[str, str] = field(default_factory=dict) image: str = "" @@ -126,6 +125,14 @@ class ClusterConfiguration: extended_resource_mapping: Dict[str, str] = field(default_factory=dict) overwrite_default_resource_mapping: bool = False local_queue: Optional[str] = None + annotations: Dict[str, str] = field(default_factory=dict) + volumes: list[V1Volume] = field(default_factory=list) + volume_mounts: list[V1VolumeMount] = field(default_factory=list) + enable_gcs_ft: bool = False + enable_usage_stats: bool = False + redis_address: Optional[str] = None + redis_password_secret: Optional[Dict[str, str]] = None + external_storage_namespace: Optional[str] = None def __post_init__(self): if not self.verify_tls: @@ -133,12 +140,35 @@ def __post_init__(self): "Warning: TLS verification has been disabled - Endpoint checks will be bypassed" ) + if self.enable_usage_stats: + self.envs["RAY_USAGE_STATS_ENABLED"] = "1" + else: + self.envs["RAY_USAGE_STATS_ENABLED"] = "0" + + if self.enable_gcs_ft: + if not self.redis_address: + raise ValueError( + "redis_address must be provided when enable_gcs_ft is True" + ) + + if self.redis_password_secret and not isinstance( + self.redis_password_secret, dict + ): + raise ValueError( + "redis_password_secret must be a dictionary with 'name' and 'key' fields" + ) + + if self.redis_password_secret and ( + "name" not in self.redis_password_secret + or "key" not in self.redis_password_secret + ): + raise ValueError( + "redis_password_secret must contain both 'name' and 'key' fields" + ) + self._validate_types() - self._memory_to_resource() self._memory_to_string() self._str_mem_no_unit_add_GB() - self._cpu_to_resource() - self._gpu_to_resource() self._combine_extended_resource_mapping() self._validate_extended_resource_requests(self.head_extended_resource_requests) self._validate_extended_resource_requests( @@ -170,29 +200,7 @@ def _validate_extended_resource_requests(self, extended_resources: Dict[str, int f"extended resource '{k}' not found in extended_resource_mapping, available resources are {list(self.extended_resource_mapping.keys())}, to add more supported resources use extended_resource_mapping. i.e. extended_resource_mapping = {{'{k}': 'FOO_BAR'}}" ) - def _gpu_to_resource(self): - if self.head_gpus: - warnings.warn( - f"head_gpus is being deprecated, replacing with head_extended_resource_requests['nvidia.com/gpu'] = {self.head_gpus}" - ) - if "nvidia.com/gpu" in self.head_extended_resource_requests: - raise ValueError( - "nvidia.com/gpu already exists in head_extended_resource_requests" - ) - self.head_extended_resource_requests["nvidia.com/gpu"] = self.head_gpus - if self.num_gpus: - warnings.warn( - f"num_gpus is being deprecated, replacing with worker_extended_resource_requests['nvidia.com/gpu'] = {self.num_gpus}" - ) - if "nvidia.com/gpu" in self.worker_extended_resource_requests: - raise ValueError( - "nvidia.com/gpu already exists in worker_extended_resource_requests" - ) - self.worker_extended_resource_requests["nvidia.com/gpu"] = self.num_gpus - def _str_mem_no_unit_add_GB(self): - if isinstance(self.head_memory, str) and self.head_memory.isdecimal(): - self.head_memory = f"{self.head_memory}G" if ( isinstance(self.worker_memory_requests, str) and self.worker_memory_requests.isdecimal() @@ -214,41 +222,17 @@ def _memory_to_string(self): if isinstance(self.worker_memory_limits, int): self.worker_memory_limits = f"{self.worker_memory_limits}G" - def _cpu_to_resource(self): - if self.head_cpus: - warnings.warn( - "head_cpus is being deprecated, use head_cpu_requests and head_cpu_limits" - ) - self.head_cpu_requests = self.head_cpu_limits = self.head_cpus - if self.min_cpus: - warnings.warn("min_cpus is being deprecated, use worker_cpu_requests") - self.worker_cpu_requests = self.min_cpus - if self.max_cpus: - warnings.warn("max_cpus is being deprecated, use worker_cpu_limits") - self.worker_cpu_limits = self.max_cpus - - def _memory_to_resource(self): - if self.head_memory: - warnings.warn( - "head_memory is being deprecated, use head_memory_requests and head_memory_limits" - ) - self.head_memory_requests = self.head_memory_limits = self.head_memory - if self.min_memory: - warnings.warn("min_memory is being deprecated, use worker_memory_requests") - self.worker_memory_requests = f"{self.min_memory}G" - if self.max_memory: - warnings.warn("max_memory is being deprecated, use worker_memory_limits") - self.worker_memory_limits = f"{self.max_memory}G" - def _validate_types(self): """Validate the types of all fields in the ClusterConfiguration dataclass.""" + errors = [] for field_info in fields(self): value = getattr(self, field_info.name) expected_type = field_info.type if not self._is_type(value, expected_type): - raise TypeError( - f"'{field_info.name}' should be of type {expected_type}" - ) + errors.append(f"'{field_info.name}' should be of type {expected_type}.") + + if errors: + raise TypeError("Type validation failed:\n" + "\n".join(errors)) @staticmethod def _is_type(value, expected_type): @@ -260,14 +244,24 @@ def check_type(value, expected_type): if origin_type is Union: return any(check_type(value, union_type) for union_type in args) if origin_type is list: - return all(check_type(elem, args[0]) for elem in value) + if value is not None: + return all(check_type(elem, args[0]) for elem in (value or [])) + else: + return True if origin_type is dict: - return all( - check_type(k, args[0]) and check_type(v, args[1]) - for k, v in value.items() - ) + if value is not None: + return all( + check_type(k, args[0]) and check_type(v, args[1]) + for k, v in value.items() + ) + else: + return True if origin_type is tuple: return all(check_type(elem, etype) for elem, etype in zip(value, args)) + if expected_type is int: + return isinstance(value, int) and not isinstance(value, bool) + if expected_type is bool: + return isinstance(value, bool) return isinstance(value, expected_type) return check_type(value, expected_type) diff --git a/src/codeflare_sdk/ray/cluster/pretty_print.py b/src/codeflare_sdk/ray/cluster/pretty_print.py index 883f14ad..faa03258 100644 --- a/src/codeflare_sdk/ray/cluster/pretty_print.py +++ b/src/codeflare_sdk/ray/cluster/pretty_print.py @@ -30,7 +30,11 @@ def print_no_resources_found(): console = Console() - console.print(Panel("[red]No resources found, have you run cluster.up() yet?")) + console.print( + Panel( + "[red]No resources found, have you run cluster.apply() yet? Run cluster.details() to check if it's ready." + ) + ) def print_app_wrappers_status(app_wrappers: List[AppWrapper], starting: bool = False): diff --git a/src/codeflare_sdk/ray/cluster/test_build_ray_cluster.py b/src/codeflare_sdk/ray/cluster/test_build_ray_cluster.py index 7d6d3d0a..3a7947d3 100644 --- a/src/codeflare_sdk/ray/cluster/test_build_ray_cluster.py +++ b/src/codeflare_sdk/ray/cluster/test_build_ray_cluster.py @@ -13,8 +13,9 @@ # limitations under the License. from collections import namedtuple import sys -from .build_ray_cluster import gen_names, update_image +from .build_ray_cluster import gen_names, update_image, build_ray_cluster import uuid +from codeflare_sdk.ray.cluster.cluster import ClusterConfiguration, Cluster def test_gen_names_with_name(mocker): @@ -39,10 +40,10 @@ def test_gen_names_without_name(mocker): def test_update_image_without_supported_python_version(mocker): # Mock SUPPORTED_PYTHON_VERSIONS mocker.patch.dict( - "codeflare_sdk.ray.cluster.build_ray_cluster.SUPPORTED_PYTHON_VERSIONS", + "codeflare_sdk.common.utils.constants.SUPPORTED_PYTHON_VERSIONS", { - "3.9": "ray-py3.9", "3.11": "ray-py3.11", + "3.12": "ray-py3.12", }, ) @@ -60,8 +61,50 @@ def test_update_image_without_supported_python_version(mocker): # Assert that the warning was called with the expected message warn_mock.assert_called_once_with( - "No default Ray image defined for 3.8. Please provide your own image or use one of the following python versions: 3.9, 3.11." + "No default Ray image defined for 3.8. Please provide your own image or use one of the following python versions: 3.11, 3.12." ) # Assert that no image was set since the Python version is not supported assert image is None + + +def test_build_ray_cluster_with_gcs_ft(mocker): + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch("kubernetes.client.CustomObjectsApi.list_namespaced_custom_object") + + cluster = Cluster( + ClusterConfiguration( + name="test", + namespace="ns", + enable_gcs_ft=True, + redis_address="redis:6379", + redis_password_secret={"name": "redis-password-secret", "key": "password"}, + external_storage_namespace="new-ns", + ) + ) + + mocker.patch("codeflare_sdk.ray.cluster.build_ray_cluster.config_check") + mocker.patch( + "codeflare_sdk.ray.cluster.build_ray_cluster.get_api_client", return_value=None + ) + mocker.patch( + "codeflare_sdk.ray.cluster.build_ray_cluster.update_image", return_value=None + ) + + resource = build_ray_cluster(cluster) + + assert "spec" in resource + assert "gcsFaultToleranceOptions" in resource["spec"] + + gcs_ft_options = resource["spec"]["gcsFaultToleranceOptions"] + + assert gcs_ft_options["redisAddress"] == "redis:6379" + assert gcs_ft_options["externalStorageNamespace"] == "new-ns" + assert ( + gcs_ft_options["redisPassword"]["valueFrom"]["secretKeyRef"]["name"] + == "redis-password-secret" + ) + assert ( + gcs_ft_options["redisPassword"]["valueFrom"]["secretKeyRef"]["key"] + == "password" + ) diff --git a/src/codeflare_sdk/ray/cluster/test_cluster.py b/src/codeflare_sdk/ray/cluster/test_cluster.py index 5e83c82a..c8742a3e 100644 --- a/src/codeflare_sdk/ray/cluster/test_cluster.py +++ b/src/codeflare_sdk/ray/cluster/test_cluster.py @@ -19,34 +19,40 @@ list_all_queued, ) from codeflare_sdk.common.utils.unit_test_support import ( - createClusterWithConfig, + create_cluster, arg_check_del_effect, ingress_retrieval, arg_check_apply_effect, get_local_queue, - createClusterConfig, + create_cluster_config, get_ray_obj, get_obj_none, get_ray_obj_with_status, get_aw_obj_with_status, + patch_cluster_with_dynamic_client, + route_list_retrieval, ) from codeflare_sdk.ray.cluster.cluster import _is_openshift_cluster from pathlib import Path from unittest.mock import MagicMock from kubernetes import client import yaml +import pytest import filecmp import os +import ray +import tempfile parent = Path(__file__).resolve().parents[4] # project directory expected_clusters_dir = f"{parent}/tests/test_cluster_yamls" aw_dir = os.path.expanduser("~/.codeflare/resources/") -def test_cluster_up_down(mocker): +def test_cluster_apply_down(mocker): mocker.patch("kubernetes.client.ApisApi.get_api_versions") mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") mocker.patch("codeflare_sdk.ray.cluster.cluster.Cluster._throw_for_no_raycluster") + mocker.patch("codeflare_sdk.ray.cluster.cluster.Cluster.get_dynamic_client") mocker.patch( "kubernetes.client.CustomObjectsApi.get_cluster_custom_object", return_value={"spec": {"domain": ""}}, @@ -67,15 +73,194 @@ def test_cluster_up_down(mocker): "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), ) - cluster = cluster = createClusterWithConfig(mocker) - cluster.up() + cluster = create_cluster(mocker) + cluster.apply() cluster.down() -def test_cluster_up_down_no_mcad(mocker): +def test_cluster_apply_scale_up_scale_down(mocker): + mocker.patch("kubernetes.client.ApisApi.get_api_versions") + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mock_dynamic_client = mocker.Mock() + mocker.patch( + "kubernetes.dynamic.DynamicClient.resources", new_callable=mocker.PropertyMock + ) + mocker.patch( + "codeflare_sdk.ray.cluster.cluster.Cluster.create_resource", + return_value="./tests/test_cluster_yamls/ray/default-ray-cluster.yaml", + ) + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch( + "kubernetes.client.CustomObjectsApi.get_cluster_custom_object", + return_value={"spec": {"domain": "apps.cluster.awsroute.org"}}, + ) + + # Initialize test + initial_num_workers = 1 + scaled_up_num_workers = 2 + + # Step 1: Create cluster with initial workers + cluster = create_cluster(mocker, initial_num_workers) + patch_cluster_with_dynamic_client(mocker, cluster, mock_dynamic_client) + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value=get_obj_none("ray.io", "v1", "ns", "rayclusters"), + ) + cluster.apply() + + # Step 2: Scale up the cluster + cluster = create_cluster(mocker, scaled_up_num_workers) + patch_cluster_with_dynamic_client(mocker, cluster, mock_dynamic_client) + cluster.apply() + + # Step 3: Scale down the cluster + cluster = create_cluster(mocker, initial_num_workers) + patch_cluster_with_dynamic_client(mocker, cluster, mock_dynamic_client) + cluster.apply() + + # Tear down + cluster.down() + + +def test_cluster_apply_with_file(mocker): + mocker.patch("kubernetes.client.ApisApi.get_api_versions") + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mock_dynamic_client = mocker.Mock() + mocker.patch("codeflare_sdk.ray.cluster.cluster.Cluster._throw_for_no_raycluster") + mocker.patch( + "kubernetes.dynamic.DynamicClient.resources", new_callable=mocker.PropertyMock + ) + mocker.patch( + "codeflare_sdk.ray.cluster.cluster.Cluster.create_resource", + return_value="./tests/test_cluster_yamls/ray/default-ray-cluster.yaml", + ) + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch( + "kubernetes.client.CustomObjectsApi.get_cluster_custom_object", + return_value={"spec": {"domain": "apps.cluster.awsroute.org"}}, + ) + + # Step 1: Create cluster with initial workers + cluster = create_cluster(mocker, 1, write_to_file=True) + patch_cluster_with_dynamic_client(mocker, cluster, mock_dynamic_client) + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value=get_obj_none("ray.io", "v1", "ns", "rayclusters"), + ) + cluster.apply() + # Tear down + cluster.down() + + +def test_cluster_apply_with_appwrapper(mocker): + # Mock Kubernetes client and dynamic client methods + mocker.patch("kubernetes.client.ApisApi.get_api_versions") + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch( + "codeflare_sdk.ray.cluster.cluster._check_aw_exists", + return_value=True, + ) + mock_dynamic_client = mocker.Mock() mocker.patch("codeflare_sdk.ray.cluster.cluster.Cluster._throw_for_no_raycluster") + mocker.patch( + "kubernetes.dynamic.DynamicClient.resources", new_callable=mocker.PropertyMock + ) + mocker.patch( + "codeflare_sdk.ray.cluster.cluster.Cluster.create_resource", + return_value="./tests/test_cluster_yamls/ray/default-ray-cluster.yaml", + ) mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + + # Create a cluster configuration with appwrapper set to False + cluster = create_cluster(mocker, 1, write_to_file=False) + patch_cluster_with_dynamic_client(mocker, cluster, mock_dynamic_client) + + # Mock listing RayCluster to simulate it doesn't exist + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value=get_obj_none("ray.io", "v1", "ns", "rayclusters"), + ) + # Call the apply method + cluster.apply() + + # Assertions + print("Cluster applied without AppWrapper.") + + +def test_cluster_apply_without_appwrapper_write_to_file(mocker): + # Mock Kubernetes client and dynamic client methods mocker.patch("kubernetes.client.ApisApi.get_api_versions") + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch( + "codeflare_sdk.ray.cluster.cluster._check_aw_exists", + return_value=True, + ) + mock_dynamic_client = mocker.Mock() + mocker.patch("codeflare_sdk.ray.cluster.cluster.Cluster._throw_for_no_raycluster") + mocker.patch( + "kubernetes.dynamic.DynamicClient.resources", new_callable=mocker.PropertyMock + ) + mocker.patch( + "codeflare_sdk.ray.cluster.cluster.Cluster.create_resource", + return_value="./tests/test_cluster_yamls/ray/default-ray-cluster.yaml", + ) + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + + # Create a cluster configuration with appwrapper set to False + cluster = create_cluster(mocker, 1, write_to_file=True) + patch_cluster_with_dynamic_client(mocker, cluster, mock_dynamic_client) + cluster.config.appwrapper = False + + # Mock listing RayCluster to simulate it doesn't exist + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value=get_obj_none("ray.io", "v1", "ns", "rayclusters"), + ) + # Call the apply method + cluster.apply() + + # Assertions + print("Cluster applied without AppWrapper.") + + +def test_cluster_apply_without_appwrapper(mocker): + # Mock Kubernetes client and dynamic client methods + mocker.patch("kubernetes.client.ApisApi.get_api_versions") + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mock_dynamic_client = mocker.Mock() + mocker.patch("codeflare_sdk.ray.cluster.cluster.Cluster._throw_for_no_raycluster") + mocker.patch( + "kubernetes.dynamic.DynamicClient.resources", new_callable=mocker.PropertyMock + ) + mocker.patch( + "codeflare_sdk.ray.cluster.cluster.Cluster.create_resource", + return_value="./tests/test_cluster_yamls/ray/default-ray-cluster.yaml", + ) + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + + # Create a cluster configuration with appwrapper set to False + cluster = create_cluster(mocker, 1, write_to_file=False) + cluster.config.appwrapper = None + patch_cluster_with_dynamic_client(mocker, cluster, mock_dynamic_client) + + # Mock listing RayCluster to simulate it doesn't exist + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value=get_obj_none("ray.io", "v1", "ns", "rayclusters"), + ) + + # Call the apply method + cluster.apply() + + # Assertions + print("Cluster applied without AppWrapper.") + + +def test_cluster_apply_down_no_mcad(mocker): + mocker.patch("codeflare_sdk.ray.cluster.cluster.Cluster._throw_for_no_raycluster") + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch("kubernetes.client.ApisApi.get_api_versions") + mocker.patch("codeflare_sdk.ray.cluster.cluster.Cluster.get_dynamic_client") mocker.patch( "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), @@ -98,11 +283,11 @@ def test_cluster_up_down_no_mcad(mocker): "kubernetes.client.CustomObjectsApi.list_cluster_custom_object", return_value={"items": []}, ) - config = createClusterConfig() + config = create_cluster_config() config.name = "unit-test-cluster-ray" config.appwrapper = False cluster = Cluster(config) - cluster.up() + cluster.apply() cluster.down() @@ -117,7 +302,7 @@ def test_cluster_uris(mocker): "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), ) - cluster = cluster = createClusterWithConfig(mocker) + cluster = create_cluster(mocker) mocker.patch( "kubernetes.client.NetworkingV1Api.list_namespaced_ingress", return_value=ingress_retrieval( @@ -144,13 +329,57 @@ def test_cluster_uris(mocker): ) assert ( cluster.cluster_dashboard_uri() - == "Dashboard not available yet, have you run cluster.up()?" + == "Dashboard not available yet, have you run cluster.apply()? Run cluster.details() to check if it's ready." ) + mocker.patch( + "codeflare_sdk.ray.cluster.cluster._is_openshift_cluster", return_value=True + ) + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value={ + "items": [ + { + "metadata": { + "name": "ray-dashboard-unit-test-cluster", + }, + "spec": { + "host": "ray-dashboard-unit-test-cluster-ns.apps.cluster.awsroute.org", + "tls": {}, # Indicating HTTPS + }, + } + ] + }, + ) + cluster = create_cluster(mocker) + assert ( + cluster.cluster_dashboard_uri() + == "http://ray-dashboard-unit-test-cluster-ns.apps.cluster.awsroute.org" + ) + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value={ + "items": [ + { + "metadata": { + "name": "ray-dashboard-unit-test-cluster", + }, + "spec": { + "host": "ray-dashboard-unit-test-cluster-ns.apps.cluster.awsroute.org", + "tls": {"termination": "passthrough"}, # Indicating HTTPS + }, + } + ] + }, + ) + cluster = create_cluster(mocker) + assert ( + cluster.cluster_dashboard_uri() + == "https://ray-dashboard-unit-test-cluster-ns.apps.cluster.awsroute.org" + ) -def test_ray_job_wrapping(mocker): - import ray +def test_ray_job_wrapping(mocker): def ray_addr(self, *args): return self._address @@ -159,7 +388,7 @@ def ray_addr(self, *args): "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), ) - cluster = cluster = createClusterWithConfig(mocker) + cluster = create_cluster(mocker) mocker.patch( "ray.job_submission.JobSubmissionClient._check_connection_and_version_with_url", return_value="None", @@ -239,11 +468,10 @@ def test_get_cluster_no_appwrapper(mocker): return_value=expected_rc, ) get_cluster("test-all-params", "ns", write_to_file=True) - assert filecmp.cmp( - f"{aw_dir}test-all-params.yaml", - f"{expected_clusters_dir}/ray/unit-test-all-params.yaml", - shallow=True, - ) + + with open(f"{aw_dir}test-all-params.yaml") as f: + generated_rc = yaml.load(f, Loader=yaml.FullLoader) + assert generated_rc == expected_rc def test_get_cluster_with_appwrapper(mocker): @@ -261,11 +489,10 @@ def test_get_cluster_with_appwrapper(mocker): return_value=expected_aw, ) get_cluster("aw-all-params", "ns", write_to_file=True) - assert filecmp.cmp( - f"{aw_dir}aw-all-params.yaml", - f"{expected_clusters_dir}/appwrapper/unit-test-all-params.yaml", - shallow=True, - ) + + with open(f"{aw_dir}aw-all-params.yaml") as f: + generated_aw = yaml.load(f, Loader=yaml.FullLoader) + assert generated_aw == expected_aw def test_wait_ready(mocker, capsys): @@ -314,7 +541,7 @@ def test_wait_ready(mocker, capsys): captured = capsys.readouterr() assert ( - "WARNING: Current cluster status is unknown, have you run cluster.up yet?" + "WARNING: Current cluster status is unknown, have you run cluster.apply() yet? Run cluster.details() to check if it's ready." in captured.out ) mocker.patch( @@ -345,11 +572,15 @@ def test_list_queue_appwrappers(mocker, capsys): ) list_all_queued("ns", appwrapper=True) captured = capsys.readouterr() - assert captured.out == ( - "╭──────────────────────────────────────────────────────────────────────────────╮\n" - "│ No resources found, have you run cluster.up() yet? │\n" - "╰──────────────────────────────────────────────────────────────────────────────╯\n" - ) + # The Rich library's console width detection varies between test contexts + # Accept either the two-line format (individual tests) or single-line format (full test suite) + # Check for key parts of the message instead of the full text + assert "No resources found" in captured.out + assert "cluster.apply()" in captured.out + assert "cluster.details()" in captured.out + assert "check if it's ready" in captured.out + assert "╭" in captured.out and "╮" in captured.out # Check for box characters + assert "│" in captured.out # Check for vertical lines mocker.patch( "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_aw_obj_with_status( @@ -390,11 +621,15 @@ def test_list_queue_rayclusters(mocker, capsys): list_all_queued("ns") captured = capsys.readouterr() - assert captured.out == ( - "╭──────────────────────────────────────────────────────────────────────────────╮\n" - "│ No resources found, have you run cluster.up() yet? │\n" - "╰──────────────────────────────────────────────────────────────────────────────╯\n" - ) + # The Rich library's console width detection varies between test contexts + # Accept either the two-line format (individual tests) or single-line format (full test suite) + # Check for key parts of the message instead of the full text + assert "No resources found" in captured.out + assert "cluster.apply()" in captured.out + assert "cluster.details()" in captured.out + assert "check if it's ready" in captured.out + assert "╭" in captured.out and "╮" in captured.out # Check for box characters + assert "│" in captured.out # Check for vertical lines mocker.patch( "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", return_value=get_ray_obj_with_status("ray.io", "v1", "ns", "rayclusters"), @@ -432,11 +667,15 @@ def test_list_clusters(mocker, capsys): ) list_all_clusters("ns") captured = capsys.readouterr() - assert captured.out == ( - "╭──────────────────────────────────────────────────────────────────────────────╮\n" - "│ No resources found, have you run cluster.up() yet? │\n" - "╰──────────────────────────────────────────────────────────────────────────────╯\n" - ) + # The Rich library's console width detection varies between test contexts + # Accept either the two-line format (individual tests) or single-line format (full test suite) + # Check for key parts of the message instead of the full text + assert "No resources found" in captured.out + assert "cluster.apply()" in captured.out + assert "cluster.details()" in captured.out + assert "check if it's ready" in captured.out + assert "╭" in captured.out and "╮" in captured.out # Check for box characters + assert "│" in captured.out # Check for vertical lines mocker.patch( "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", side_effect=get_ray_obj, @@ -532,7 +771,542 @@ def custom_side_effect(group, version, namespace, plural, **kwargs): assert result.dashboard == rc_dashboard +def test_throw_for_no_raycluster_crd_errors(mocker): + """Test RayCluster CRD error handling""" + from kubernetes.client.rest import ApiException + + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + + # Test 404 error - CRD not found + mock_api_404 = MagicMock() + mock_api_404.list_namespaced_custom_object.side_effect = ApiException(status=404) + mocker.patch("kubernetes.client.CustomObjectsApi", return_value=mock_api_404) + + cluster = create_cluster(mocker) + with pytest.raises( + RuntimeError, match="RayCluster CustomResourceDefinition unavailable" + ): + cluster._throw_for_no_raycluster() + + # Test other API error + mock_api_500 = MagicMock() + mock_api_500.list_namespaced_custom_object.side_effect = ApiException(status=500) + mocker.patch("kubernetes.client.CustomObjectsApi", return_value=mock_api_500) + + cluster2 = create_cluster(mocker) + with pytest.raises( + RuntimeError, match="Failed to get RayCluster CustomResourceDefinition" + ): + cluster2._throw_for_no_raycluster() + + +def test_cluster_apply_attribute_error_handling(mocker): + """Test AttributeError handling when DynamicClient fails""" + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch("codeflare_sdk.ray.cluster.cluster.Cluster._throw_for_no_raycluster") + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), + ) + + # Mock get_dynamic_client to raise AttributeError + def raise_attribute_error(): + raise AttributeError("DynamicClient initialization failed") + + mocker.patch( + "codeflare_sdk.ray.cluster.cluster.Cluster.get_dynamic_client", + side_effect=raise_attribute_error, + ) + + cluster = create_cluster(mocker) + + with pytest.raises(RuntimeError, match="Failed to initialize DynamicClient"): + cluster.apply() + + +def test_cluster_namespace_handling(mocker, capsys): + """Test namespace validation in create_resource""" + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), + ) + + # Test with None namespace that gets set + mocker.patch( + "codeflare_sdk.ray.cluster.cluster.get_current_namespace", return_value=None + ) + + config = ClusterConfiguration( + name="test-cluster-ns", + namespace=None, # Will trigger namespace check + num_workers=1, + worker_cpu_requests=1, + worker_cpu_limits=1, + worker_memory_requests=2, + worker_memory_limits=2, + ) + + cluster = Cluster(config) + captured = capsys.readouterr() + # Verify the warning message was printed + assert "Please specify with namespace=" in captured.out + assert cluster.config.namespace is None + + +def test_component_resources_with_write_to_file(mocker): + """Test _component_resources_up with write_to_file enabled""" + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), + ) + + # Mock the _create_resources function + mocker.patch("codeflare_sdk.ray.cluster.cluster._create_resources") + + # Create cluster with write_to_file=True (without appwrapper) + config = ClusterConfiguration( + name="test-cluster-component", + namespace="ns", + num_workers=1, + worker_cpu_requests=1, + worker_cpu_limits=1, + worker_memory_requests=2, + worker_memory_limits=2, + write_to_file=True, + appwrapper=False, + ) + + cluster = Cluster(config) + + # Mock file reading and test _component_resources_up + + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + f.write("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test") + temp_file = f.name + + try: + mock_api = MagicMock() + cluster.resource_yaml = temp_file + cluster._component_resources_up("ns", mock_api) + # If we got here without error, the write_to_file path was executed + assert True + finally: + os.unlink(temp_file) + + +def test_get_cluster_status_functions(mocker): + """Test _app_wrapper_status and _ray_cluster_status functions""" + from codeflare_sdk.ray.cluster.cluster import ( + _app_wrapper_status, + _ray_cluster_status, + ) + + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch("codeflare_sdk.ray.cluster.cluster.config_check") + + # Test _app_wrapper_status when cluster not found + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value={"items": []}, + ) + result = _app_wrapper_status("non-existent-cluster", "ns") + assert result is None + + # Test _ray_cluster_status when cluster not found + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value={"items": []}, + ) + result = _ray_cluster_status("non-existent-cluster", "ns") + assert result is None + + +def test_cluster_namespace_type_error(mocker): + """Test TypeError when namespace is not a string""" + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), + ) + + # Mock get_current_namespace to return a non-string value (e.g., int) + mocker.patch( + "codeflare_sdk.ray.cluster.cluster.get_current_namespace", return_value=12345 + ) + + config = ClusterConfiguration( + name="test-cluster-type-error", + namespace=None, # Will trigger namespace check + num_workers=1, + worker_cpu_requests=1, + worker_cpu_limits=1, + worker_memory_requests=2, + worker_memory_limits=2, + ) + + # This should raise TypeError because get_current_namespace returns int + with pytest.raises( + TypeError, + match="Namespace 12345 is of type.*Check your Kubernetes Authentication", + ): + Cluster(config) + + +def test_get_dashboard_url_from_httproute(mocker): + """ + Test the HTTPRoute dashboard URL generation for RHOAI v3.0+ + """ + from codeflare_sdk.ray.cluster.cluster import _get_dashboard_url_from_httproute + + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + + # Test successful HTTPRoute and Gateway lookup + mock_httproute = { + "metadata": {"name": "test-cluster", "namespace": "test-ns"}, + "spec": { + "parentRefs": [ + { + "group": "gateway.networking.k8s.io", + "kind": "Gateway", + "name": "data-science-gateway", + "namespace": "openshift-ingress", + } + ] + }, + } + + mock_gateway = { + "metadata": {"name": "data-science-gateway", "namespace": "openshift-ingress"}, + "spec": { + "listeners": [ + { + "name": "https", + "hostname": "data-science-gateway.apps.example.com", + "port": 443, + "protocol": "HTTPS", + } + ] + }, + } + + # Mock the CustomObjectsApi to return HTTPRoute and Gateway + def mock_get_namespaced_custom_object(group, version, namespace, plural, name): + if plural == "httproutes": + return mock_httproute + elif plural == "gateways": + return mock_gateway + raise Exception("Unexpected plural") + + mocker.patch( + "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object", + side_effect=mock_get_namespaced_custom_object, + ) + + # Test successful URL generation + result = _get_dashboard_url_from_httproute("test-cluster", "test-ns") + expected_url = ( + "https://data-science-gateway.apps.example.com/ray/test-ns/test-cluster" + ) + assert result == expected_url, f"Expected {expected_url}, got {result}" + + # Test HTTPRoute not found (404) - should return None + def mock_404_error(group, version, namespace, plural, name): + error = client.exceptions.ApiException(status=404) + error.status = 404 + raise error + + mocker.patch( + "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object", + side_effect=mock_404_error, + ) + + result = _get_dashboard_url_from_httproute("nonexistent-cluster", "test-ns") + assert result is None, "Should return None when HTTPRoute not found" + + # Test HTTPRoute with empty parentRefs - should return None + mock_httproute_no_parents = { + "metadata": {"name": "test-cluster", "namespace": "test-ns"}, + "spec": {"parentRefs": []}, # Empty parentRefs + } + + def mock_httproute_no_parents_fn(group, version, namespace, plural, name): + if plural == "httproutes": + return mock_httproute_no_parents + raise Exception("Unexpected plural") + + mocker.patch( + "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object", + side_effect=mock_httproute_no_parents_fn, + ) + + result = _get_dashboard_url_from_httproute("test-cluster", "test-ns") + assert result is None, "Should return None when HTTPRoute has empty parentRefs" + + # Test HTTPRoute with missing gateway name - should return None + mock_httproute_no_name = { + "metadata": {"name": "test-cluster", "namespace": "test-ns"}, + "spec": { + "parentRefs": [ + { + "group": "gateway.networking.k8s.io", + "kind": "Gateway", + # Missing "name" field + "namespace": "openshift-ingress", + } + ] + }, + } + + def mock_httproute_no_name_fn(group, version, namespace, plural, name): + if plural == "httproutes": + return mock_httproute_no_name + raise Exception("Unexpected plural") + + mocker.patch( + "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object", + side_effect=mock_httproute_no_name_fn, + ) + + result = _get_dashboard_url_from_httproute("test-cluster", "test-ns") + assert result is None, "Should return None when gateway reference missing name" + + # Test HTTPRoute with missing gateway namespace - should return None + mock_httproute_no_namespace = { + "metadata": {"name": "test-cluster", "namespace": "test-ns"}, + "spec": { + "parentRefs": [ + { + "group": "gateway.networking.k8s.io", + "kind": "Gateway", + "name": "data-science-gateway", + # Missing "namespace" field + } + ] + }, + } + + def mock_httproute_no_namespace_fn(group, version, namespace, plural, name): + if plural == "httproutes": + return mock_httproute_no_namespace + raise Exception("Unexpected plural") + + mocker.patch( + "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object", + side_effect=mock_httproute_no_namespace_fn, + ) + + result = _get_dashboard_url_from_httproute("test-cluster", "test-ns") + assert result is None, "Should return None when gateway reference missing namespace" + + # Test Gateway with empty listeners - should return None + mock_httproute_valid = { + "metadata": {"name": "test-cluster", "namespace": "test-ns"}, + "spec": { + "parentRefs": [ + { + "group": "gateway.networking.k8s.io", + "kind": "Gateway", + "name": "data-science-gateway", + "namespace": "openshift-ingress", + } + ] + }, + } + + mock_gateway_no_listeners = { + "metadata": {"name": "data-science-gateway", "namespace": "openshift-ingress"}, + "spec": {"listeners": []}, # Empty listeners + } + + def mock_gateway_no_listeners_fn(group, version, namespace, plural, name): + if plural == "httproutes": + return mock_httproute_valid + elif plural == "gateways": + return mock_gateway_no_listeners + raise Exception("Unexpected plural") + + mocker.patch( + "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object", + side_effect=mock_gateway_no_listeners_fn, + ) + + result = _get_dashboard_url_from_httproute("test-cluster", "test-ns") + assert result is None, "Should return None when Gateway has empty listeners" + + # Test Gateway listener with missing hostname - should return None + mock_gateway_no_hostname = { + "metadata": {"name": "data-science-gateway", "namespace": "openshift-ingress"}, + "spec": { + "listeners": [ + { + "name": "https", + # Missing "hostname" field + "port": 443, + "protocol": "HTTPS", + } + ] + }, + } + + def mock_gateway_no_hostname_fn(group, version, namespace, plural, name): + if plural == "httproutes": + return mock_httproute_valid + elif plural == "gateways": + return mock_gateway_no_hostname + raise Exception("Unexpected plural") + + mocker.patch( + "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object", + side_effect=mock_gateway_no_hostname_fn, + ) + + result = _get_dashboard_url_from_httproute("test-cluster", "test-ns") + assert result is None, "Should return None when listener missing hostname" + + # Test non-404 ApiException - should be re-raised then caught by outer handler + # The function is designed to return None for any unexpected errors via outer try-catch + def mock_403_error(group, version, namespace, plural, name): + error = client.exceptions.ApiException(status=403) + error.status = 403 + raise error + + mocker.patch( + "kubernetes.client.CustomObjectsApi.get_namespaced_custom_object", + side_effect=mock_403_error, + ) + + # Should return None (the inner handler re-raises, outer handler catches and returns None) + result = _get_dashboard_url_from_httproute("test-cluster", "test-ns") + assert ( + result is None + ), "Should return None when non-404 exception occurs (caught by outer handler)" + + +def test_cluster_dashboard_uri_httproute_first(mocker): + """ + Test that cluster_dashboard_uri() tries HTTPRoute first, then falls back to OpenShift Routes + """ + mocker.patch("kubernetes.client.ApisApi.get_api_versions") + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value=get_local_queue("kueue.x-k8s.io", "v1beta1", "ns", "localqueues"), + ) + + # Test 1: HTTPRoute exists - should return HTTPRoute URL + mocker.patch( + "codeflare_sdk.ray.cluster.cluster._is_openshift_cluster", return_value=True + ) + + httproute_url = ( + "https://data-science-gateway.apps.example.com/ray/ns/unit-test-cluster" + ) + mocker.patch( + "codeflare_sdk.ray.cluster.cluster._get_dashboard_url_from_httproute", + return_value=httproute_url, + ) + + cluster = create_cluster(mocker) + result = cluster.cluster_dashboard_uri() + assert result == httproute_url, "Should return HTTPRoute URL when available" + + # Test 2: HTTPRoute not found - should fall back to OpenShift Route + mocker.patch( + "codeflare_sdk.ray.cluster.cluster._get_dashboard_url_from_httproute", + return_value=None, + ) + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value={ + "items": [ + { + "metadata": {"name": "ray-dashboard-unit-test-cluster"}, + "spec": { + "host": "ray-dashboard-unit-test-cluster-ns.apps.cluster.awsroute.org", + "tls": {"termination": "passthrough"}, + }, + } + ] + }, + ) + + cluster = create_cluster(mocker) + result = cluster.cluster_dashboard_uri() + expected = "https://ray-dashboard-unit-test-cluster-ns.apps.cluster.awsroute.org" + assert ( + result == expected + ), f"Should fall back to OpenShift Route. Expected {expected}, got {result}" + + +def test_map_to_ray_cluster_httproute(mocker): + """ + Test that _map_to_ray_cluster() uses HTTPRoute-first logic + """ + from codeflare_sdk.ray.cluster.cluster import _map_to_ray_cluster + + mocker.patch("kubernetes.config.load_kube_config", return_value="ignore") + mocker.patch( + "codeflare_sdk.ray.cluster.cluster._is_openshift_cluster", return_value=True + ) + + # Test with HTTPRoute available + httproute_url = ( + "https://data-science-gateway.apps.example.com/ray/ns/test-cluster-a" + ) + mocker.patch( + "codeflare_sdk.ray.cluster.cluster._get_dashboard_url_from_httproute", + return_value=httproute_url, + ) + + rc = get_ray_obj("ray.io", "v1", "ns", "rayclusters")["items"][0] + result = _map_to_ray_cluster(rc) + + assert ( + result.dashboard == httproute_url + ), f"Expected HTTPRoute URL, got {result.dashboard}" + + # Test with HTTPRoute not available - should fall back to OpenShift Route + mocker.patch( + "codeflare_sdk.ray.cluster.cluster._get_dashboard_url_from_httproute", + return_value=None, + ) + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + return_value={ + "items": [ + { + "kind": "Route", + "metadata": { + "name": "ray-dashboard-test-cluster-a", + "namespace": "ns", + }, + "spec": {"host": "ray-dashboard-test-cluster-a.apps.example.com"}, + } + ] + }, + ) + + rc = get_ray_obj("ray.io", "v1", "ns", "rayclusters")["items"][0] + result = _map_to_ray_cluster(rc) + + expected_fallback = "http://ray-dashboard-test-cluster-a.apps.example.com" + assert ( + result.dashboard == expected_fallback + ), f"Expected OpenShift Route fallback URL, got {result.dashboard}" + + # Make sure to always keep this function last def test_cleanup(): - os.remove(f"{aw_dir}test-all-params.yaml") - os.remove(f"{aw_dir}aw-all-params.yaml") + # Clean up test files if they exist + # Using try-except to handle cases where files weren't created (e.g., when running full test suite) + try: + os.remove(f"{aw_dir}test-all-params.yaml") + except FileNotFoundError: + pass # File doesn't exist, nothing to clean up + + try: + os.remove(f"{aw_dir}aw-all-params.yaml") + except FileNotFoundError: + pass # File doesn't exist, nothing to clean up diff --git a/src/codeflare_sdk/ray/cluster/test_config.py b/src/codeflare_sdk/ray/cluster/test_config.py index 1423fc2b..e405bc5b 100644 --- a/src/codeflare_sdk/ray/cluster/test_config.py +++ b/src/codeflare_sdk/ray/cluster/test_config.py @@ -13,16 +13,18 @@ # limitations under the License. from codeflare_sdk.common.utils.unit_test_support import ( - createClusterWrongType, - get_local_queue, + apply_template, + get_example_extended_storage_opts, + create_cluster_wrong_type, create_cluster_all_config_params, + get_template_variables, ) from codeflare_sdk.ray.cluster.cluster import ClusterConfiguration, Cluster from pathlib import Path import filecmp import pytest -import yaml import os +import yaml parent = Path(__file__).resolve().parents[4] # project directory expected_clusters_dir = f"{parent}/tests/test_cluster_yamls" @@ -36,9 +38,12 @@ def test_default_cluster_creation(mocker): cluster = Cluster(ClusterConfiguration(name="default-cluster", namespace="ns")) - with open(f"{expected_clusters_dir}/ray/default-ray-cluster.yaml") as f: - expected_rc = yaml.load(f, Loader=yaml.FullLoader) - assert cluster.resource_yaml == expected_rc + expected_rc = apply_template( + f"{expected_clusters_dir}/ray/default-ray-cluster.yaml", + get_template_variables(), + ) + + assert cluster.resource_yaml == expected_rc def test_default_appwrapper_creation(mocker): @@ -50,17 +55,20 @@ def test_default_appwrapper_creation(mocker): ClusterConfiguration(name="default-appwrapper", namespace="ns", appwrapper=True) ) - with open(f"{expected_clusters_dir}/ray/default-appwrapper.yaml") as f: - expected_aw = yaml.load(f, Loader=yaml.FullLoader) - assert cluster.resource_yaml == expected_aw + expected_aw = apply_template( + f"{expected_clusters_dir}/ray/default-appwrapper.yaml", get_template_variables() + ) + assert cluster.resource_yaml == expected_aw +@pytest.mark.filterwarnings("ignore::UserWarning") def test_config_creation_all_parameters(mocker): from codeflare_sdk.ray.cluster.config import DEFAULT_RESOURCE_MAPPING expected_extended_resource_mapping = DEFAULT_RESOURCE_MAPPING expected_extended_resource_mapping.update({"example.com/gpu": "GPU"}) expected_extended_resource_mapping["intel.com/gpu"] = "TPU" + volumes, volume_mounts = get_example_extended_storage_opts() cluster = create_cluster_all_config_params(mocker, "test-all-params", False) assert cluster.config.name == "test-all-params" and cluster.config.namespace == "ns" @@ -78,7 +86,11 @@ def test_config_creation_all_parameters(mocker): assert cluster.config.worker_memory_requests == "12G" assert cluster.config.worker_memory_limits == "16G" assert cluster.config.appwrapper == False - assert cluster.config.envs == {"key1": "value1", "key2": "value2"} + assert cluster.config.envs == { + "key1": "value1", + "key2": "value2", + "RAY_USAGE_STATS_ENABLED": "0", + } assert cluster.config.image == "example/ray:tag" assert cluster.config.image_pull_secrets == ["secret1", "secret2"] assert cluster.config.write_to_file == True @@ -90,6 +102,13 @@ def test_config_creation_all_parameters(mocker): ) assert cluster.config.overwrite_default_resource_mapping == True assert cluster.config.local_queue == "local-queue-default" + assert cluster.config.annotations == { + "app.kubernetes.io/managed-by": "test-prefix", + "key1": "value1", + "key2": "value2", + } + assert cluster.config.volumes == volumes + assert cluster.config.volume_mounts == volume_mounts assert filecmp.cmp( f"{aw_dir}test-all-params.yaml", @@ -98,8 +117,10 @@ def test_config_creation_all_parameters(mocker): ) +@pytest.mark.filterwarnings("ignore::UserWarning") def test_all_config_params_aw(mocker): create_cluster_all_config_params(mocker, "aw-all-params", True) + assert filecmp.cmp( f"{aw_dir}aw-all-params.yaml", f"{expected_clusters_dir}/appwrapper/unit-test-all-params.yaml", @@ -108,32 +129,101 @@ def test_all_config_params_aw(mocker): def test_config_creation_wrong_type(): - with pytest.raises(TypeError): - createClusterWrongType() + with pytest.raises(TypeError) as error_info: + create_cluster_wrong_type() + + assert len(str(error_info.value).splitlines()) == 4 -def test_cluster_config_deprecation_conversion(mocker): +def test_gcs_fault_tolerance_config_validation(): config = ClusterConfiguration( name="test", - num_gpus=2, - head_gpus=1, - head_cpus=3, - head_memory=16, - min_memory=3, - max_memory=4, - min_cpus=1, - max_cpus=2, + namespace="ns", + enable_gcs_ft=True, + redis_address="redis:6379", + redis_password_secret={"name": "redis-password-secret", "key": "password"}, + external_storage_namespace="new-ns", ) - assert config.head_cpu_requests == 3 - assert config.head_cpu_limits == 3 - assert config.head_memory_requests == "16G" - assert config.head_memory_limits == "16G" - assert config.worker_extended_resource_requests == {"nvidia.com/gpu": 2} - assert config.head_extended_resource_requests == {"nvidia.com/gpu": 1} - assert config.worker_memory_requests == "3G" - assert config.worker_memory_limits == "4G" - assert config.worker_cpu_requests == 1 - assert config.worker_cpu_limits == 2 + + assert config.enable_gcs_ft is True + assert config.redis_address == "redis:6379" + assert config.redis_password_secret == { + "name": "redis-password-secret", + "key": "password", + } + assert config.external_storage_namespace == "new-ns" + + try: + ClusterConfiguration(name="test", namespace="ns", enable_gcs_ft=True) + except ValueError as e: + assert str(e) in "redis_address must be provided when enable_gcs_ft is True" + + try: + ClusterConfiguration( + name="test", + namespace="ns", + enable_gcs_ft=True, + redis_address="redis:6379", + redis_password_secret={"secret"}, + ) + except ValueError as e: + assert ( + str(e) + in "redis_password_secret must be a dictionary with 'name' and 'key' fields" + ) + + try: + ClusterConfiguration( + name="test", + namespace="ns", + enable_gcs_ft=True, + redis_address="redis:6379", + redis_password_secret={"wrong": "format"}, + ) + except ValueError as e: + assert ( + str(e) in "redis_password_secret must contain both 'name' and 'key' fields" + ) + + +def test_ray_usage_stats_default(mocker): + mocker.patch("kubernetes.client.ApisApi.get_api_versions") + mocker.patch("kubernetes.client.CustomObjectsApi.list_namespaced_custom_object") + + cluster = Cluster( + ClusterConfiguration(name="default-usage-stats-cluster", namespace="ns") + ) + + # Verify that usage stats are disabled by default + assert cluster.config.envs["RAY_USAGE_STATS_ENABLED"] == "0" + + # Check that the environment variable is set in the YAML + head_container = cluster.resource_yaml["spec"]["headGroupSpec"]["template"]["spec"][ + "containers" + ][0] + env_vars = {env["name"]: env["value"] for env in head_container["env"]} + assert env_vars["RAY_USAGE_STATS_ENABLED"] == "0" + + +def test_ray_usage_stats_enabled(mocker): + mocker.patch("kubernetes.client.ApisApi.get_api_versions") + mocker.patch("kubernetes.client.CustomObjectsApi.list_namespaced_custom_object") + + cluster = Cluster( + ClusterConfiguration( + name="usage-stats-enabled-cluster", + namespace="ns", + enable_usage_stats=True, + ) + ) + + assert cluster.config.envs["RAY_USAGE_STATS_ENABLED"] == "1" + + head_container = cluster.resource_yaml["spec"]["headGroupSpec"]["template"]["spec"][ + "containers" + ][0] + env_vars = {env["name"]: env["value"] for env in head_container["env"]} + assert env_vars["RAY_USAGE_STATS_ENABLED"] == "1" # Make sure to always keep this function last diff --git a/src/codeflare_sdk/ray/cluster/test_pretty_print.py b/src/codeflare_sdk/ray/cluster/test_pretty_print.py index 329a1354..f36e290c 100644 --- a/src/codeflare_sdk/ray/cluster/test_pretty_print.py +++ b/src/codeflare_sdk/ray/cluster/test_pretty_print.py @@ -38,11 +38,15 @@ def test_print_no_resources(capsys): except Exception: assert 1 == 0 captured = capsys.readouterr() - assert captured.out == ( - "╭──────────────────────────────────────────────────────────────────────────────╮\n" - "│ No resources found, have you run cluster.up() yet? │\n" - "╰──────────────────────────────────────────────────────────────────────────────╯\n" - ) + # The Rich library's console width detection varies between test contexts + # Accept either the two-line format (individual tests) or single-line format (full test suite) + # Check for key parts of the message instead of the full text + assert "No resources found" in captured.out + assert "cluster.apply()" in captured.out + assert "cluster.details()" in captured.out + assert "check if it's ready" in captured.out + assert "╭" in captured.out and "╮" in captured.out # Check for box characters + assert "│" in captured.out # Check for vertical lines def test_print_appwrappers(capsys): diff --git a/src/codeflare_sdk/ray/rayjobs/__init__.py b/src/codeflare_sdk/ray/rayjobs/__init__.py new file mode 100644 index 00000000..cd6b4123 --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/__init__.py @@ -0,0 +1,3 @@ +from .rayjob import RayJob, ManagedClusterConfig +from .status import RayJobDeploymentStatus, CodeflareRayJobStatus, RayJobInfo +from .config import ManagedClusterConfig diff --git a/src/codeflare_sdk/ray/rayjobs/config.py b/src/codeflare_sdk/ray/rayjobs/config.py new file mode 100644 index 00000000..5b724272 --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/config.py @@ -0,0 +1,502 @@ +# Copyright 2022 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The config sub-module contains the definition of the ManagedClusterConfig dataclass, +which is used to specify resource requirements and other details when creating a +Cluster object. +""" + +import pathlib +from dataclasses import dataclass, field, fields +from typing import Dict, List, Optional, Union, get_args, get_origin, Any, Tuple +from kubernetes.client import ( + V1LocalObjectReference, + V1SecretVolumeSource, + V1Toleration, + V1Volume, + V1VolumeMount, + V1ObjectMeta, + V1Container, + V1ContainerPort, + V1Lifecycle, + V1ExecAction, + V1LifecycleHandler, + V1EnvVar, + V1PodTemplateSpec, + V1PodSpec, + V1ResourceRequirements, +) + +import logging + +from ...common.utils.constants import MOUNT_PATH, RAY_VERSION +from ...common.utils.utils import update_image + +logger = logging.getLogger(__name__) + +dir = pathlib.Path(__file__).parent.parent.resolve() + +# https://docs.ray.io/en/latest/ray-core/scheduling/accelerators.html +DEFAULT_ACCELERATORS = { + "nvidia.com/gpu": "GPU", + "intel.com/gpu": "GPU", + "amd.com/gpu": "GPU", + "aws.amazon.com/neuroncore": "neuron_cores", + "google.com/tpu": "TPU", + "habana.ai/gaudi": "HPU", + "huawei.com/Ascend910": "NPU", + "huawei.com/Ascend310": "NPU", +} + + +@dataclass +class ManagedClusterConfig: + """ + This dataclass is used to specify resource requirements and other details for RayJobs. + The cluster name and namespace are automatically derived from the RayJob configuration. + + Args: + head_accelerators: + A dictionary of extended resource requests for the head node. ex: {"nvidia.com/gpu": 1} + head_tolerations: + List of tolerations for head nodes. + num_workers: + The number of workers to create. + worker_tolerations: + List of tolerations for worker nodes. + envs: + A dictionary of environment variables to set for the cluster. + image: + The image to use for the cluster. + image_pull_secrets: + A list of image pull secrets to use for the cluster. + labels: + A dictionary of labels to apply to the cluster. + worker_accelerators: + A dictionary of extended resource requests for each worker. ex: {"nvidia.com/gpu": 1} + accelerator_configs: + A dictionary of custom resource mappings to map extended resource requests to RayCluster resource names. + Defaults to DEFAULT_ACCELERATORS but can be overridden with custom mappings. + annotations: + A dictionary of annotations to apply to the Job. + volumes: + A list of V1Volume objects to add to the Cluster + volume_mounts: + A list of V1VolumeMount objects to add to the Cluster + """ + + head_cpu_requests: Union[int, str] = 2 + head_cpu_limits: Union[int, str] = 2 + head_memory_requests: Union[int, str] = 8 + head_memory_limits: Union[int, str] = 8 + head_accelerators: Dict[str, Union[str, int]] = field(default_factory=dict) + head_tolerations: Optional[List[V1Toleration]] = None + worker_cpu_requests: Union[int, str] = 1 + worker_cpu_limits: Union[int, str] = 1 + num_workers: int = 1 + worker_memory_requests: Union[int, str] = 2 + worker_memory_limits: Union[int, str] = 2 + worker_tolerations: Optional[List[V1Toleration]] = None + envs: Dict[str, str] = field(default_factory=dict) + image: str = "" + image_pull_secrets: List[str] = field(default_factory=list) + labels: Dict[str, str] = field(default_factory=dict) + worker_accelerators: Dict[str, Union[str, int]] = field(default_factory=dict) + accelerator_configs: Dict[str, str] = field( + default_factory=lambda: DEFAULT_ACCELERATORS.copy() + ) + annotations: Dict[str, str] = field(default_factory=dict) + volumes: list[V1Volume] = field(default_factory=list) + volume_mounts: list[V1VolumeMount] = field(default_factory=list) + + def __post_init__(self): + self.envs["RAY_USAGE_STATS_ENABLED"] = "0" + + self._validate_types() + self._memory_to_string() + self._validate_gpu_config(self.head_accelerators) + self._validate_gpu_config(self.worker_accelerators) + + def _validate_gpu_config(self, gpu_config: Dict[str, int]): + for k in gpu_config.keys(): + if k not in self.accelerator_configs.keys(): + raise ValueError( + f"GPU configuration '{k}' not found in accelerator_configs, available resources are {list(self.accelerator_configs.keys())}, to add more supported resources use accelerator_configs. i.e. accelerator_configs = {{'{k}': 'FOO_BAR'}}" + ) + + def _memory_to_string(self): + if isinstance(self.head_memory_requests, int): + self.head_memory_requests = f"{self.head_memory_requests}G" + if isinstance(self.head_memory_limits, int): + self.head_memory_limits = f"{self.head_memory_limits}G" + if isinstance(self.worker_memory_requests, int): + self.worker_memory_requests = f"{self.worker_memory_requests}G" + if isinstance(self.worker_memory_limits, int): + self.worker_memory_limits = f"{self.worker_memory_limits}G" + + def _validate_types(self): + """Validate the types of all fields in the ManagedClusterConfig dataclass.""" + errors = [] + for field_info in fields(self): + value = getattr(self, field_info.name) + expected_type = field_info.type + if not self._is_type(value, expected_type): + errors.append(f"'{field_info.name}' should be of type {expected_type}.") + + if errors: + raise TypeError("Type validation failed:\n" + "\n".join(errors)) + + @staticmethod + def _is_type(value, expected_type): + """Check if the value matches the expected type.""" + + def check_type(value, expected_type): + origin_type = get_origin(expected_type) + args = get_args(expected_type) + if origin_type is Union: + return any(check_type(value, union_type) for union_type in args) + if origin_type is list: + if value is not None: + return all(check_type(elem, args[0]) for elem in (value or [])) + else: + return True + if origin_type is dict: + if value is not None: + return all( + check_type(k, args[0]) and check_type(v, args[1]) + for k, v in value.items() + ) + else: + return True + if origin_type is tuple: + return all(check_type(elem, etype) for elem, etype in zip(value, args)) + if expected_type is int: + return isinstance(value, int) and not isinstance(value, bool) + if expected_type is bool: + return isinstance(value, bool) + return isinstance(value, expected_type) + + return check_type(value, expected_type) + + def build_ray_cluster_spec(self, cluster_name: str) -> Dict[str, Any]: + """ + Build the RayCluster spec from ManagedClusterConfig for embedding in RayJob. + + Args: + self: The cluster configuration object (ManagedClusterConfig) + cluster_name: The name for the cluster (derived from RayJob name) + + Returns: + Dict containing the RayCluster spec for embedding in RayJob + """ + ray_cluster_spec = { + "rayVersion": RAY_VERSION, + "enableInTreeAutoscaling": False, # Required for Kueue-managed jobs + "headGroupSpec": self._build_head_group_spec(), + "workerGroupSpecs": [self._build_worker_group_spec(cluster_name)], + } + + return ray_cluster_spec + + def _build_head_group_spec(self) -> Dict[str, Any]: + """Build the head group specification.""" + return { + "serviceType": "ClusterIP", + "enableIngress": False, + "rayStartParams": self._build_head_ray_params(), + "template": V1PodTemplateSpec( + metadata=V1ObjectMeta(annotations=self.annotations), + spec=self._build_pod_spec(self._build_head_container(), is_head=True), + ), + } + + def _build_worker_group_spec(self, cluster_name: str) -> Dict[str, Any]: + """Build the worker group specification.""" + return { + "replicas": self.num_workers, + "minReplicas": self.num_workers, + "maxReplicas": self.num_workers, + "groupName": f"worker-group-{cluster_name}", + "rayStartParams": self._build_worker_ray_params(), + "template": V1PodTemplateSpec( + metadata=V1ObjectMeta(annotations=self.annotations), + spec=self._build_pod_spec( + self._build_worker_container(), + is_head=False, + ), + ), + } + + def _build_head_ray_params(self) -> Dict[str, str]: + """Build Ray start parameters for head node.""" + params = { + "dashboard-host": "0.0.0.0", + "block": "true", + } + + # Add GPU count if specified + if self.head_accelerators: + gpu_count = sum( + count + for resource_type, count in self.head_accelerators.items() + if "gpu" in resource_type.lower() + ) + if gpu_count > 0: + params["num-gpus"] = str(gpu_count) + + return params + + def _build_worker_ray_params(self) -> Dict[str, str]: + """Build Ray start parameters for worker nodes.""" + params = { + "block": "true", + } + + # Add GPU count if specified + if self.worker_accelerators: + gpu_count = sum( + count + for resource_type, count in self.worker_accelerators.items() + if "gpu" in resource_type.lower() + ) + if gpu_count > 0: + params["num-gpus"] = str(gpu_count) + + return params + + def _build_head_container(self) -> V1Container: + """Build the head container specification.""" + container = V1Container( + name="ray-head", + image=update_image(self.image), + image_pull_policy="IfNotPresent", # Always IfNotPresent for RayJobs + ports=[ + V1ContainerPort(name="gcs", container_port=6379), + V1ContainerPort(name="dashboard", container_port=8265), + V1ContainerPort(name="client", container_port=10001), + ], + lifecycle=V1Lifecycle( + pre_stop=V1LifecycleHandler( + _exec=V1ExecAction(command=["/bin/sh", "-c", "ray stop"]) + ) + ), + resources=self._build_resource_requirements( + self.head_cpu_requests, + self.head_cpu_limits, + self.head_memory_requests, + self.head_memory_limits, + self.head_accelerators, + ), + volume_mounts=self._generate_volume_mounts(), + env=self._build_env_vars() if hasattr(self, "envs") and self.envs else None, + ) + + return container + + def _build_worker_container(self) -> V1Container: + """Build the worker container specification.""" + container = V1Container( + name="ray-worker", + image=update_image(self.image), + image_pull_policy="IfNotPresent", # Always IfNotPresent for RayJobs + lifecycle=V1Lifecycle( + pre_stop=V1LifecycleHandler( + _exec=V1ExecAction(command=["/bin/sh", "-c", "ray stop"]) + ) + ), + resources=self._build_resource_requirements( + self.worker_cpu_requests, + self.worker_cpu_limits, + self.worker_memory_requests, + self.worker_memory_limits, + self.worker_accelerators, + ), + volume_mounts=self._generate_volume_mounts(), + env=self._build_env_vars() if hasattr(self, "envs") and self.envs else None, + ) + + return container + + def _build_resource_requirements( + self, + cpu_requests: Union[int, str], + cpu_limits: Union[int, str], + memory_requests: Union[int, str], + memory_limits: Union[int, str], + extended_resource_requests: Dict[str, Union[int, str]] = None, + ) -> V1ResourceRequirements: + """Build Kubernetes resource requirements.""" + resource_requirements = V1ResourceRequirements( + requests={"cpu": cpu_requests, "memory": memory_requests}, + limits={"cpu": cpu_limits, "memory": memory_limits}, + ) + + # Add extended resources (e.g., GPUs) + if extended_resource_requests: + for resource_type, amount in extended_resource_requests.items(): + resource_requirements.limits[resource_type] = amount + resource_requirements.requests[resource_type] = amount + + return resource_requirements + + def _build_pod_spec(self, container: V1Container, is_head: bool) -> V1PodSpec: + """Build the pod specification.""" + pod_spec = V1PodSpec( + containers=[container], + volumes=self._generate_volumes(), + restart_policy="Never", # RayJobs should not restart + ) + + # Add tolerations if specified + if is_head and hasattr(self, "head_tolerations") and self.head_tolerations: + pod_spec.tolerations = self.head_tolerations + elif ( + not is_head + and hasattr(self, "worker_tolerations") + and self.worker_tolerations + ): + pod_spec.tolerations = self.worker_tolerations + + # Add image pull secrets if specified + if hasattr(self, "image_pull_secrets") and self.image_pull_secrets: + pod_spec.image_pull_secrets = [ + V1LocalObjectReference(name=secret) + for secret in self.image_pull_secrets + ] + + return pod_spec + + def _generate_volume_mounts(self) -> list: + """Generate volume mounts for the container.""" + volume_mounts = [] + + # Add custom volume mounts if specified + if hasattr(self, "volume_mounts") and self.volume_mounts: + volume_mounts.extend(self.volume_mounts) + + return volume_mounts + + def _generate_volumes(self) -> list: + """Generate volumes for the pod.""" + volumes = [] + + # Add custom volumes if specified + if hasattr(self, "volumes") and self.volumes: + volumes.extend(self.volumes) + + return volumes + + def _build_env_vars(self) -> list: + """Build environment variables list.""" + return [V1EnvVar(name=key, value=value) for key, value in self.envs.items()] + + def add_file_volumes(self, secret_name: str, mount_path: str = MOUNT_PATH): + """ + Add file volume and mount references to cluster configuration. + + Args: + secret_name: Name of the Secret containing files + mount_path: Where to mount files in containers (default: /home/ray/scripts) + """ + # Check if file volume already exists + volume_name = "ray-job-files" + existing_volume = next( + (v for v in self.volumes if getattr(v, "name", None) == volume_name), None + ) + if existing_volume: + logger.debug(f"File volume '{volume_name}' already exists, skipping...") + return + + # Check if file mount already exists + existing_mount = next( + (m for m in self.volume_mounts if getattr(m, "name", None) == volume_name), + None, + ) + if existing_mount: + logger.debug( + f"File volume mount '{volume_name}' already exists, skipping..." + ) + return + + # Add file volume to cluster configuration + file_volume = V1Volume( + name=volume_name, secret=V1SecretVolumeSource(secret_name=secret_name) + ) + self.volumes.append(file_volume) + + # Add file volume mount to cluster configuration + file_mount = V1VolumeMount(name=volume_name, mount_path=mount_path) + self.volume_mounts.append(file_mount) + + logger.info( + f"Added file volume '{secret_name}' to cluster config: mount_path={mount_path}" + ) + + def validate_secret_size(self, files: Dict[str, str]) -> None: + total_size = sum(len(content.encode("utf-8")) for content in files.values()) + if total_size > 1024 * 1024: # 1MB + raise ValueError( + f"Secret size exceeds 1MB limit. Total size: {total_size} bytes" + ) + + def build_file_secret_spec( + self, job_name: str, namespace: str, files: Dict[str, str] + ) -> Dict[str, Any]: + """ + Build Secret specification for files + + Args: + job_name: Name of the RayJob (used for Secret naming) + namespace: Kubernetes namespace + files: Dictionary of file_name -> file_content + + Returns: + Dict: Secret specification ready for Kubernetes API + """ + secret_name = f"{job_name}-files" + return { + "apiVersion": "v1", + "kind": "Secret", + "type": "Opaque", + "metadata": { + "name": secret_name, + "namespace": namespace, + "labels": { + "ray.io/job-name": job_name, + "app.kubernetes.io/managed-by": "codeflare-sdk", + "app.kubernetes.io/component": "rayjob-files", + }, + }, + "data": files, + } + + def build_file_volume_specs( + self, secret_name: str, mount_path: str = MOUNT_PATH + ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + """ + Build volume and mount specifications for files + + Args: + secret_name: Name of the Secret containing files + mount_path: Where to mount files in containers + + Returns: + Tuple of (volume_spec, mount_spec) as dictionaries + """ + volume_spec = {"name": "ray-job-files", "secret": {"secretName": secret_name}} + + mount_spec = {"name": "ray-job-files", "mountPath": mount_path} + + return volume_spec, mount_spec diff --git a/src/codeflare_sdk/ray/rayjobs/pretty_print.py b/src/codeflare_sdk/ray/rayjobs/pretty_print.py new file mode 100644 index 00000000..34e8dfa1 --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/pretty_print.py @@ -0,0 +1,117 @@ +# Copyright 2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This sub-module exists primarily to be used internally by the RayJob object +(in the rayjob sub-module) for pretty-printing job status and details. +""" + +from rich.console import Console +from rich.table import Table +from rich.panel import Panel +from typing import Tuple, Optional + +from .status import RayJobDeploymentStatus, RayJobInfo + + +def print_job_status(job_info: RayJobInfo): + """ + Pretty print the job status in a format similar to cluster status. + """ + status_display, header_color = _get_status_display(job_info.status) + + # Create main info table + table = _create_info_table(header_color, job_info.name, status_display) + table.add_row(f"[bold]Job ID:[/bold] {job_info.job_id}") + table.add_row(f"[bold]Status:[/bold] {job_info.status.value}") + table.add_row(f"[bold]RayCluster:[/bold] {job_info.cluster_name}") + table.add_row(f"[bold]Namespace:[/bold] {job_info.namespace}") + + # Add timing information if available + if job_info.start_time: + table.add_row() + table.add_row(f"[bold]Started:[/bold] {job_info.start_time}") + + # Add attempt counts if there are failures + if job_info.failed_attempts > 0: + table.add_row(f"[bold]Failed Attempts:[/bold] {job_info.failed_attempts}") + + _print_table_in_panel(table) + + +def print_no_job_found(job_name: str, namespace: str): + """ + Print a message when no job is found. + """ + # Create table with error message + table = _create_info_table( + "[white on red][bold]Name", job_name, "[bold red]No RayJob found" + ) + table.add_row() + table.add_row("Please run rayjob.submit() to submit a job.") + table.add_row() + table.add_row(f"[bold]Namespace:[/bold] {namespace}") + + _print_table_in_panel(table) + + +def _get_status_display(status: RayJobDeploymentStatus) -> Tuple[str, str]: + """ + Get the display string and header color for a given status. + + Returns: + Tuple of (status_display, header_color) + """ + status_mapping = { + RayJobDeploymentStatus.COMPLETE: ( + "Complete :white_heavy_check_mark:", + "[white on green][bold]Name", + ), + RayJobDeploymentStatus.RUNNING: ("Running :gear:", "[white on blue][bold]Name"), + RayJobDeploymentStatus.FAILED: ("Failed :x:", "[white on red][bold]Name"), + RayJobDeploymentStatus.SUSPENDED: ( + "Suspended :pause_button:", + "[white on yellow][bold]Name", + ), + } + + return status_mapping.get( + status, ("Unknown :question:", "[white on red][bold]Name") + ) + + +def _create_info_table(header_color: str, name: str, status_display: str) -> Table: + """ + Create a standardized info table with header and status. + + Returns: + Table with header row, name/status row, and empty separator row + """ + table = Table(box=None, show_header=False) + table.add_row(header_color) + table.add_row("[bold underline]" + name, status_display) + table.add_row() # Empty separator row + return table + + +def _print_table_in_panel(table: Table): + """ + Print a table wrapped in a consistent panel format. + """ + console = Console() + main_table = Table( + box=None, title="[bold] :package: CodeFlare RayJob Status :package:" + ) + main_table.add_row(Panel.fit(table)) + console.print(main_table) diff --git a/src/codeflare_sdk/ray/rayjobs/rayjob.py b/src/codeflare_sdk/ray/rayjobs/rayjob.py new file mode 100644 index 00000000..c06c596e --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/rayjob.py @@ -0,0 +1,615 @@ +# Copyright 2022-2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +RayJob client for submitting and managing Ray jobs using the kuberay python client. +""" + +import logging +import os +import re +import warnings +from typing import Dict, Any, Optional, Tuple, Union + +from ray.runtime_env import RuntimeEnv +from codeflare_sdk.common.kueue.kueue import get_default_kueue_name +from codeflare_sdk.common.utils.constants import MOUNT_PATH + +from codeflare_sdk.common.utils.utils import get_ray_image_for_python_version +from codeflare_sdk.vendored.python_client.kuberay_job_api import RayjobApi +from codeflare_sdk.vendored.python_client.kuberay_cluster_api import RayClusterApi +from codeflare_sdk.ray.rayjobs.config import ManagedClusterConfig +from codeflare_sdk.ray.rayjobs.runtime_env import ( + create_file_secret, + extract_all_local_files, + process_runtime_env, +) + +from ...common.utils import get_current_namespace +from ...common.utils.validation import validate_ray_version_compatibility + +from .status import ( + RayJobDeploymentStatus, + CodeflareRayJobStatus, + RayJobInfo, +) +from . import pretty_print + + +logger = logging.getLogger(__name__) + + +class RayJob: + """ + A client for managing Ray jobs using the KubeRay operator. + + This class provides a simplified interface for submitting and managing + RayJob CRs (using the KubeRay RayJob python client). + """ + + def __init__( + self, + job_name: str, + entrypoint: str, + cluster_name: Optional[str] = None, + cluster_config: Optional[ManagedClusterConfig] = None, + namespace: Optional[str] = None, + runtime_env: Optional[Union[RuntimeEnv, Dict[str, Any]]] = None, + ttl_seconds_after_finished: int = 0, + active_deadline_seconds: Optional[int] = None, + local_queue: Optional[str] = None, + ): + """ + Initialize a RayJob instance. + + Args: + job_name: The name for the Ray job + entrypoint: The Python script or command to run (required) + cluster_name: The name of an existing Ray cluster (optional if cluster_config provided) + cluster_config: Configuration for creating a new cluster (optional if cluster_name provided) + namespace: The Kubernetes namespace (auto-detected if not specified) + runtime_env: Ray runtime environment configuration. Can be: + - RuntimeEnv object from ray.runtime_env + - Dict with keys like 'working_dir', 'pip', 'env_vars', etc. + Example: {"working_dir": "./my-scripts", "pip": ["requests"]} + ttl_seconds_after_finished: Seconds to wait before cleanup after job finishes (default: 0) + active_deadline_seconds: Maximum time the job can run before being terminated (optional) + local_queue: The Kueue LocalQueue to submit the job to (optional) + + Note: + - True if cluster_config is provided (new cluster will be cleaned up) + - False if cluster_name is provided (existing cluster will not be shut down) + - User can explicitly set this value to override auto-detection + """ + if cluster_name is None and cluster_config is None: + raise ValueError( + "❌ Configuration Error: You must provide either 'cluster_name' (for existing cluster) " + "or 'cluster_config' (to create new cluster), but not both." + ) + + if cluster_name is not None and cluster_config is not None: + raise ValueError( + "❌ Configuration Error: You cannot specify both 'cluster_name' and 'cluster_config'. " + "Choose one approach:\n" + "• Use 'cluster_name' to connect to an existing cluster\n" + "• Use 'cluster_config' to create a new cluster" + ) + + if cluster_config is None and cluster_name is None: + raise ValueError( + "❌ Configuration Error: When not providing 'cluster_config', 'cluster_name' is required " + "to specify which existing cluster to use." + ) + + self.name = job_name + self.entrypoint = entrypoint + + # Convert dict to RuntimeEnv if needed for user convenience + if isinstance(runtime_env, dict): + self.runtime_env = RuntimeEnv(**runtime_env) + else: + self.runtime_env = runtime_env + + self.ttl_seconds_after_finished = ttl_seconds_after_finished + self.active_deadline_seconds = active_deadline_seconds + self.local_queue = local_queue + + if namespace is None: + detected_namespace = get_current_namespace() + if detected_namespace: + self.namespace = detected_namespace + logger.info(f"Auto-detected namespace: {self.namespace}") + else: + raise ValueError( + "❌ Configuration Error: Could not auto-detect Kubernetes namespace. " + "Please explicitly specify the 'namespace' parameter. " + ) + else: + self.namespace = namespace + + self._cluster_name = cluster_name + self._cluster_config = cluster_config + + if cluster_config is not None: + self.cluster_name = f"{job_name}-cluster" + logger.info(f"Creating new cluster: {self.cluster_name}") + else: + # Using existing cluster: cluster_name must be provided + if cluster_name is None: + raise ValueError( + "❌ Configuration Error: a 'cluster_name' is required when not providing 'cluster_config'" + ) + self.cluster_name = cluster_name + logger.info(f"Using existing cluster: {self.cluster_name}") + + self._api = RayjobApi() + self._cluster_api = RayClusterApi() + + logger.info(f"Initialized RayJob: {self.name} in namespace: {self.namespace}") + + def submit(self) -> str: + if not self.entrypoint: + raise ValueError("Entrypoint must be provided to submit a RayJob") + + # Validate configuration before submitting + self._validate_ray_version_compatibility() + self._validate_working_dir_entrypoint() + + # Extract files from entrypoint and runtime_env working_dir + files = extract_all_local_files(self) + + rayjob_cr = self._build_rayjob_cr() + + logger.info(f"Submitting RayJob {self.name} to Kuberay operator") + result = self._api.submit_job(k8s_namespace=self.namespace, job=rayjob_cr) + + if result: + logger.info(f"Successfully submitted RayJob {self.name}") + + # Create Secret with owner reference after RayJob exists + if files: + create_file_secret(self, files, result) + + return self.name + else: + raise RuntimeError(f"Failed to submit RayJob {self.name}") + + def stop(self): + """ + Suspend the Ray job. + """ + stopped = self._api.suspend_job(name=self.name, k8s_namespace=self.namespace) + if stopped: + logger.info(f"Successfully stopped the RayJob {self.name}") + return True + else: + raise RuntimeError(f"Failed to stop the RayJob {self.name}") + + def resubmit(self): + """ + Resubmit the Ray job. + """ + if self._api.resubmit_job(name=self.name, k8s_namespace=self.namespace): + logger.info(f"Successfully resubmitted the RayJob {self.name}") + return True + else: + raise RuntimeError(f"Failed to resubmit the RayJob {self.name}") + + def delete(self): + """ + Delete the Ray job. + Returns True if deleted successfully or if already deleted. + """ + deleted = self._api.delete_job(name=self.name, k8s_namespace=self.namespace) + if deleted: + logger.info(f"Successfully deleted the RayJob {self.name}") + return True + else: + # The python client logs "rayjob custom resource already deleted" + # and returns False when the job doesn't exist. + # This is not an error - treat it as successful deletion. + logger.info(f"RayJob {self.name} already deleted or does not exist") + return True + + def _build_rayjob_cr(self) -> Dict[str, Any]: + """ + Build the RayJob custom resource specification using native RayJob capabilities. + """ + rayjob_cr = { + "apiVersion": "ray.io/v1", + "kind": "RayJob", + "metadata": { + "name": self.name, + "namespace": self.namespace, + }, + "spec": { + "entrypoint": self.entrypoint, + "ttlSecondsAfterFinished": self.ttl_seconds_after_finished, + "shutdownAfterJobFinishes": self._cluster_config is not None, + }, + } + + # Extract files once and use for both runtime_env and submitter pod + files = extract_all_local_files(self) + + labels = {} + # If cluster_config is provided, use the local_queue from the cluster_config + if self._cluster_config is not None: + if self.local_queue: + labels["kueue.x-k8s.io/queue-name"] = self.local_queue + else: + default_queue = get_default_kueue_name(self.namespace) + if default_queue: + labels["kueue.x-k8s.io/queue-name"] = default_queue + else: + # No default queue found, use "default" as fallback + labels["kueue.x-k8s.io/queue-name"] = "default" + logger.warning( + f"No default Kueue LocalQueue found in namespace '{self.namespace}'. " + f"Using 'default' as the queue name. If a LocalQueue named 'default' " + f"does not exist, the RayJob submission will fail. " + f"To fix this, please explicitly specify the 'local_queue' parameter." + ) + + rayjob_cr["metadata"]["labels"] = labels + + # When using Kueue (queue label present), start with suspend=true + # Kueue will unsuspend the job once the workload is admitted + if labels.get("kueue.x-k8s.io/queue-name"): + rayjob_cr["spec"]["suspend"] = True + + # Add active deadline if specified + if self.active_deadline_seconds: + rayjob_cr["spec"]["activeDeadlineSeconds"] = self.active_deadline_seconds + + # Add runtime environment (can be inferred even if not explicitly specified) + processed_runtime_env = process_runtime_env(self, files) + if processed_runtime_env: + rayjob_cr["spec"]["runtimeEnvYAML"] = processed_runtime_env + + # Add submitterPodTemplate if we have files to mount + if files: + secret_name = f"{self.name}-files" + rayjob_cr["spec"][ + "submitterPodTemplate" + ] = self._build_submitter_pod_template(files, secret_name) + + # Configure cluster: either use existing or create new + if self._cluster_config is not None: + ray_cluster_spec = self._cluster_config.build_ray_cluster_spec( + cluster_name=self.cluster_name + ) + + logger.info( + f"Built RayCluster spec using RayJob-specific builder for cluster: {self.cluster_name}" + ) + + rayjob_cr["spec"]["rayClusterSpec"] = ray_cluster_spec + + logger.info(f"RayJob will create new cluster: {self.cluster_name}") + else: + # Use clusterSelector to reference existing cluster + rayjob_cr["spec"]["clusterSelector"] = {"ray.io/cluster": self.cluster_name} + logger.info(f"RayJob will use existing cluster: {self.cluster_name}") + + return rayjob_cr + + def _build_submitter_pod_template( + self, files: Dict[str, str], secret_name: str + ) -> Dict[str, Any]: + """ + Build submitterPodTemplate with Secret volume mount for local files. + + If files contain working_dir.zip, an init container will unzip it before + the main submitter container runs. + + Args: + files: Dict of file_name -> file_content + secret_name: Name of the Secret containing the files + + Returns: + submitterPodTemplate specification + """ + from codeflare_sdk.ray.rayjobs.runtime_env import UNZIP_PATH + + # Image has to be hard coded for the job submitter + image = get_ray_image_for_python_version() + if ( + self._cluster_config + and hasattr(self._cluster_config, "image") + and self._cluster_config.image + ): + image = self._cluster_config.image + + # Build Secret items for each file + secret_items = [] + entrypoint_path = files.get( + "__entrypoint_path__" + ) # Metadata for single file case + + for file_name in files.keys(): + if file_name == "__entrypoint_path__": + continue # Skip metadata key + + # For single file case, use the preserved path structure + if entrypoint_path: + secret_items.append({"key": file_name, "path": entrypoint_path}) + else: + secret_items.append({"key": file_name, "path": file_name}) + + # Check if we need to unzip working_dir + has_working_dir_zip = "working_dir.zip" in files + + # Base volume mounts for main container + volume_mounts = [{"name": "ray-job-files", "mountPath": MOUNT_PATH}] + + # If we have a zip file, we need shared volume for unzipped content + if has_working_dir_zip: + volume_mounts.append( + {"name": "unzipped-working-dir", "mountPath": UNZIP_PATH} + ) + + submitter_pod_template = { + "spec": { + "restartPolicy": "Never", + "containers": [ + { + "name": "ray-job-submitter", + "image": image, + "volumeMounts": volume_mounts, + } + ], + "volumes": [ + { + "name": "ray-job-files", + "secret": { + "secretName": secret_name, + "items": secret_items, + }, + } + ], + } + } + + # Add init container and volume for unzipping if needed + if has_working_dir_zip: + # Add emptyDir volume for unzipped content + submitter_pod_template["spec"]["volumes"].append( + {"name": "unzipped-working-dir", "emptyDir": {}} + ) + + # Add init container to unzip before KubeRay's submitter runs + submitter_pod_template["spec"]["initContainers"] = [ + { + "name": "unzip-working-dir", + "image": image, + "command": ["/bin/sh", "-c"], + "args": [ + # Decode base64 zip, save to temp file, extract, cleanup + f"mkdir -p {UNZIP_PATH} && " + f"python3 -m base64 -d {MOUNT_PATH}/working_dir.zip > /tmp/working_dir.zip && " + f"python3 -m zipfile -e /tmp/working_dir.zip {UNZIP_PATH}/ && " + f"rm /tmp/working_dir.zip && " + f"echo 'Successfully unzipped working_dir to {UNZIP_PATH}' && " + f"ls -la {UNZIP_PATH}" + ], + "volumeMounts": volume_mounts, + } + ] + logger.info(f"Added init container to unzip working_dir to {UNZIP_PATH}") + + logger.info( + f"Built submitterPodTemplate with {len(files)} files mounted at {MOUNT_PATH}, using image: {image}" + ) + return submitter_pod_template + + def _validate_ray_version_compatibility(self): + """ + Validate Ray version compatibility for cluster_config image. + Raises ValueError if there is a version mismatch. + """ + # Validate cluster_config image if creating new cluster + if self._cluster_config is not None: + self._validate_cluster_config_image() + + def _validate_cluster_config_image(self): + """ + Validate that the Ray version in cluster_config image matches the SDK's Ray version. + """ + if not hasattr(self._cluster_config, "image"): + logger.debug( + "No image attribute found in cluster config, skipping validation" + ) + return + + image = self._cluster_config.image + if not image: + logger.debug("Cluster config image is empty, skipping validation") + return + + if not isinstance(image, str): + logger.warning( + f"Cluster config image should be a string, got {type(image).__name__}: {image}" + ) + return # Skip validation for malformed image + + is_compatible, is_warning, message = validate_ray_version_compatibility(image) + if not is_compatible: + raise ValueError(f"Cluster config image: {message}") + elif is_warning: + warnings.warn(f"Cluster config image: {message}") + + def _validate_working_dir_entrypoint(self): + """ + Validate entrypoint file configuration. + + Checks: + 1. Entrypoint doesn't redundantly reference working_dir + 2. Local files exist before submission + + Raises ValueError if validation fails. + """ + # Skip validation for inline commands (python -c, etc.) + if re.search(r"\s+-c\s+", self.entrypoint): + return + + # Match Python file references only + file_pattern = r"(?:python\d?\s+)?([./\w/-]+\.py)" + matches = re.findall(file_pattern, self.entrypoint) + + if not matches: + return + + entrypoint_path = matches[0] + + # Get working_dir from runtime_env + runtime_env_dict = None + working_dir = None + + if self.runtime_env: + runtime_env_dict = ( + self.runtime_env.to_dict() + if hasattr(self.runtime_env, "to_dict") + else self.runtime_env + ) + if runtime_env_dict and "working_dir" in runtime_env_dict: + working_dir = runtime_env_dict["working_dir"] + + # Skip all validation for remote working_dir + if working_dir and not os.path.isdir(working_dir): + return + + # Case 1: Local working_dir - check redundancy and file existence + if working_dir: + normalized_working_dir = os.path.normpath(working_dir) + normalized_entrypoint = os.path.normpath(entrypoint_path) + + # Check for redundant directory reference + if normalized_entrypoint.startswith(normalized_working_dir + os.sep): + relative_to_working_dir = os.path.relpath( + normalized_entrypoint, normalized_working_dir + ) + working_dir_basename = os.path.basename(normalized_working_dir) + redundant_nested_path = os.path.join( + normalized_working_dir, + working_dir_basename, + relative_to_working_dir, + ) + + if not os.path.exists(redundant_nested_path): + raise ValueError( + f"❌ Working directory conflict detected:\n" + f" working_dir: '{working_dir}'\n" + f" entrypoint references: '{entrypoint_path}'\n" + f"\n" + f"This will fail because the entrypoint runs from within working_dir.\n" + f"It would look for: '{redundant_nested_path}' (which doesn't exist)\n" + f"\n" + f"Fix: Remove the directory prefix from your entrypoint:\n" + f' entrypoint = "python {relative_to_working_dir}"' + ) + + # Check file exists within working_dir + if not normalized_entrypoint.startswith(normalized_working_dir + os.sep): + # Use normalized_working_dir (absolute path) for proper file existence check + full_entrypoint_path = os.path.join( + normalized_working_dir, entrypoint_path + ) + if not os.path.isfile(full_entrypoint_path): + raise ValueError( + f"❌ Entrypoint file not found:\n" + f" Looking for: '{full_entrypoint_path}'\n" + f" (working_dir: '{working_dir}', entrypoint file: '{entrypoint_path}')\n" + f"\n" + f"Please ensure the file exists at the expected location." + ) + + # Case 2: No working_dir - validate local file exists + else: + if not os.path.isfile(entrypoint_path): + raise ValueError( + f"❌ Entrypoint file not found: '{entrypoint_path}'\n" + f"\n" + f"Please ensure the file exists at the specified path." + ) + + def status( + self, print_to_console: bool = True + ) -> Tuple[CodeflareRayJobStatus, bool]: + """ + Get the status of the Ray job. + + Args: + print_to_console (bool): Whether to print formatted status to console (default: True) + + Returns: + Tuple of (CodeflareRayJobStatus, ready: bool) where ready indicates job completion + """ + status_data = self._api.get_job_status( + name=self.name, k8s_namespace=self.namespace + ) + + if not status_data: + if print_to_console: + pretty_print.print_no_job_found(self.name, self.namespace) + return CodeflareRayJobStatus.UNKNOWN, False + + # Map deployment status to our enums + deployment_status_str = status_data.get("jobDeploymentStatus", "Unknown") + + try: + deployment_status = RayJobDeploymentStatus(deployment_status_str) + except ValueError: + deployment_status = RayJobDeploymentStatus.UNKNOWN + + # Create RayJobInfo dataclass + job_info = RayJobInfo( + name=self.name, + job_id=status_data.get("jobId", ""), + status=deployment_status, + namespace=self.namespace, + cluster_name=self.cluster_name, + start_time=status_data.get("startTime"), + end_time=status_data.get("endTime"), + failed_attempts=status_data.get("failed", 0), + succeeded_attempts=status_data.get("succeeded", 0), + ) + + # Map to CodeFlare status and determine readiness + codeflare_status, ready = self._map_to_codeflare_status(deployment_status) + + if print_to_console: + pretty_print.print_job_status(job_info) + + return codeflare_status, ready + + def _map_to_codeflare_status( + self, deployment_status: RayJobDeploymentStatus + ) -> Tuple[CodeflareRayJobStatus, bool]: + """ + Map deployment status to CodeFlare status and determine readiness. + + Returns: + Tuple of (CodeflareRayJobStatus, ready: bool) + """ + status_mapping = { + RayJobDeploymentStatus.COMPLETE: (CodeflareRayJobStatus.COMPLETE, True), + RayJobDeploymentStatus.RUNNING: (CodeflareRayJobStatus.RUNNING, False), + RayJobDeploymentStatus.FAILED: (CodeflareRayJobStatus.FAILED, False), + RayJobDeploymentStatus.SUSPENDED: (CodeflareRayJobStatus.SUSPENDED, False), + } + + return status_mapping.get( + deployment_status, (CodeflareRayJobStatus.UNKNOWN, False) + ) diff --git a/src/codeflare_sdk/ray/rayjobs/runtime_env.py b/src/codeflare_sdk/ray/rayjobs/runtime_env.py new file mode 100644 index 00000000..93606d73 --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/runtime_env.py @@ -0,0 +1,405 @@ +from __future__ import annotations # Postpone evaluation of annotations + +import logging +import os +import re +import yaml +import zipfile +import base64 +import io +from typing import Dict, Any, Optional, List, TYPE_CHECKING +from codeflare_sdk.common.utils.constants import MOUNT_PATH +from kubernetes import client +from ray.runtime_env import RuntimeEnv + +from codeflare_sdk.ray.rayjobs.config import ManagedClusterConfig +from ...common.kubernetes_cluster.auth import get_api_client + +# Use TYPE_CHECKING to avoid circular import at runtime +if TYPE_CHECKING: + from codeflare_sdk.ray.rayjobs.rayjob import RayJob + +logger = logging.getLogger(__name__) + +# Regex pattern for finding Python files in entrypoint commands +# Matches paths like: test.py, ./test.py, dir/test.py, my-dir/test.py +PYTHON_FILE_PATTERN = r"(?:python\s+)?([./\w/-]+\.py)" + +# Path where working_dir will be unzipped on submitter pod +UNZIP_PATH = "/tmp/rayjob-working-dir" + +# Exclude Jupyter notebook and Markdown files from working directory zips +JUPYTER_NOTEBOOK_PATTERN = r"\.ipynb$" +MARKDOWN_FILE_PATTERN = r"\.md$" + + +def _should_exclude_file(file_path: str) -> bool: + """ + Check if file should be excluded from working directory zip. + Currently excludes: + - Jupyter notebook files (.ipynb) + - Markdown files (.md) + + Args: + file_path: Relative file path within the working directory + + Returns: + True if file should be excluded, False otherwise + """ + return bool( + re.search(JUPYTER_NOTEBOOK_PATTERN, file_path, re.IGNORECASE) + or re.search(MARKDOWN_FILE_PATTERN, file_path, re.IGNORECASE) + ) + + +def _normalize_runtime_env( + runtime_env: Optional[RuntimeEnv], +) -> Optional[Dict[str, Any]]: + if runtime_env is None: + return None + return runtime_env.to_dict() + + +def extract_all_local_files(job: RayJob) -> Optional[Dict[str, str]]: + """ + Prepare local files for Secret upload. + + - If runtime_env has local working_dir: zip entire directory into single file + - If single entrypoint file (no working_dir): extract that file + - If remote working_dir URL: return None (pass through to Ray) + + Returns: + Dict with either: + - {"working_dir.zip": } for zipped directories + - {"script.py": } for single files + - None for remote working_dir or no files + """ + # Convert RuntimeEnv to dict for processing + runtime_env_dict = _normalize_runtime_env(job.runtime_env) + + # If there's a remote working_dir, don't extract local files + if ( + runtime_env_dict + and "working_dir" in runtime_env_dict + and not os.path.isdir(runtime_env_dict["working_dir"]) + ): + logger.info( + f"Remote working_dir detected: {runtime_env_dict['working_dir']}. " + "Skipping local file extraction - using remote source." + ) + return None + + # If there's a local working_dir, zip it + if ( + runtime_env_dict + and "working_dir" in runtime_env_dict + and os.path.isdir(runtime_env_dict["working_dir"]) + ): + working_dir = runtime_env_dict["working_dir"] + logger.info(f"Zipping local working_dir: {working_dir}") + zip_data = _zip_directory(working_dir) + if zip_data: + # Encode zip as base64 for Secret storage + zip_base64 = base64.b64encode(zip_data).decode("utf-8") + return {"working_dir.zip": zip_base64} + + # If no working_dir, check for single entrypoint file + entrypoint_file = _extract_single_entrypoint_file(job) + if entrypoint_file: + return entrypoint_file + + return None + + +def _zip_directory(directory_path: str) -> Optional[bytes]: + """ + Zip entire directory preserving structure, excluding Jupyter notebook and markdown files. + + Args: + directory_path: Path to directory to zip + + Returns: + Bytes of zip file, or None on error + """ + try: + # Create in-memory zip file + zip_buffer = io.BytesIO() + excluded_count = 0 + + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf: + # Walk through directory and add all files + for root, dirs, files in os.walk(directory_path): + for file in files: + file_path = os.path.join(root, file) + # Calculate relative path from directory_path + arcname = os.path.relpath(file_path, directory_path) + + # Check if file should be excluded + if _should_exclude_file(arcname): + excluded_count += 1 + logger.debug(f"Excluded from zip: {arcname}") + continue + + zipf.write(file_path, arcname) + logger.debug(f"Added to zip: {arcname}") + + zip_data = zip_buffer.getvalue() + + # Log summary with exclusion count + log_message = ( + f"Successfully zipped directory: {directory_path} ({len(zip_data)} bytes)" + ) + if excluded_count > 0: + log_message += f" - Excluded {excluded_count} file(s) (.ipynb, .md)" + logger.info(log_message) + + return zip_data + + except (IOError, OSError) as e: + logger.error(f"Failed to zip directory {directory_path}: {e}") + return None + + +def _extract_single_entrypoint_file(job: RayJob) -> Optional[Dict[str, str]]: + """ + Extract single Python file from entrypoint if no working_dir specified. + + Returns a dict with metadata about the file path structure so we can + preserve it when mounting via Secret. + + Args: + job: RayJob instance + + Returns: + Dict with special format: {"__entrypoint_path__": path, "filename": content} + This allows us to preserve directory structure when mounting + """ + if not job.entrypoint: + return None + + # Look for Python file in entrypoint + matches = re.findall(PYTHON_FILE_PATTERN, job.entrypoint) + + for file_path in matches: + # Check if it's a local file + if os.path.isfile(file_path): + try: + with open(file_path, "r") as f: + content = f.read() + + # Use basename as key (Secret keys can't have slashes) + # But store the full path for later use in Secret item.path + filename = os.path.basename(file_path) + relative_path = file_path.lstrip("./") + + logger.info(f"Extracted single entrypoint file: {file_path}") + + # Return special format with metadata + return {"__entrypoint_path__": relative_path, filename: content} + + except (IOError, OSError) as e: + logger.warning(f"Could not read entrypoint file {file_path}: {e}") + + return None + + +def process_runtime_env( + job: RayJob, files: Optional[Dict[str, str]] = None +) -> Optional[str]: + """ + Process runtime_env field to handle env_vars, pip dependencies, and working_dir. + + Returns: + Processed runtime environment as YAML string, or None if no processing needed + """ + # Convert RuntimeEnv to dict for processing + runtime_env_dict = _normalize_runtime_env(job.runtime_env) + + processed_env = {} + + # Handle env_vars + if runtime_env_dict and "env_vars" in runtime_env_dict: + processed_env["env_vars"] = runtime_env_dict["env_vars"] + logger.info( + f"Added {len(runtime_env_dict['env_vars'])} environment variables to runtime_env" + ) + + # Handle pip dependencies + if runtime_env_dict and "pip" in runtime_env_dict: + pip_deps = process_pip_dependencies(job, runtime_env_dict["pip"]) + if pip_deps: + processed_env["pip"] = pip_deps + + # Handle working_dir + if runtime_env_dict and "working_dir" in runtime_env_dict: + working_dir = runtime_env_dict["working_dir"] + if os.path.isdir(working_dir): + # Local working directory - will be zipped and unzipped to UNZIP_PATH by submitter pod + processed_env["working_dir"] = UNZIP_PATH + logger.info( + f"Local working_dir will be zipped, mounted, and unzipped to: {UNZIP_PATH}" + ) + else: + # Remote URI (e.g., GitHub, S3) - pass through as-is + processed_env["working_dir"] = working_dir + logger.info(f"Using remote working_dir: {working_dir}") + + # If no working_dir specified but we have files (single file case) + elif not runtime_env_dict or "working_dir" not in runtime_env_dict: + if files and "working_dir.zip" not in files: + # Single file case - mount at MOUNT_PATH + processed_env["working_dir"] = MOUNT_PATH + logger.info(f"Single file will be mounted at: {MOUNT_PATH}") + + # Convert to YAML string if we have any processed environment + if processed_env: + return yaml.dump(processed_env, default_flow_style=False) + + return None + + +def process_pip_dependencies(job: RayJob, pip_spec) -> Optional[List[str]]: + """ + Process pip dependencies from runtime_env. + + Args: + pip_spec: Can be a list of packages, a string path to requirements.txt, or dict + + Returns: + List of pip dependencies + """ + if isinstance(pip_spec, list): + # Already a list of dependencies + logger.info(f"Using provided pip dependencies: {len(pip_spec)} packages") + return pip_spec + elif isinstance(pip_spec, str): + # Assume it's a path to requirements.txt + return parse_requirements_file(pip_spec) + elif isinstance(pip_spec, dict): + # Handle dict format (e.g., {"packages": [...], "pip_check": False}) + if "packages" in pip_spec: + logger.info( + f"Using pip dependencies from dict: {len(pip_spec['packages'])} packages" + ) + return pip_spec["packages"] + + logger.warning(f"Unsupported pip specification format: {type(pip_spec)}") + return None + + +def parse_requirements_file(requirements_path: str) -> Optional[List[str]]: + """ + Parse a requirements.txt file and return list of dependencies. + + Args: + requirements_path: Path to requirements.txt file + + Returns: + List of pip dependencies + """ + if not os.path.isfile(requirements_path): + logger.warning(f"Requirements file not found: {requirements_path}") + return None + + try: + with open(requirements_path, "r") as f: + lines = f.readlines() + + # Parse requirements, filtering out comments and empty lines + requirements = [] + for line in lines: + line = line.strip() + if line and not line.startswith("#"): + requirements.append(line) + + logger.info(f"Parsed {len(requirements)} dependencies from {requirements_path}") + return requirements + + except (IOError, OSError) as e: + logger.warning(f"Could not read requirements file {requirements_path}: {e}") + return None + + +def create_secret_from_spec( + job: RayJob, secret_spec: Dict[str, Any], rayjob_result: Dict[str, Any] = None +) -> str: + """ + Create Secret from specification via Kubernetes API. + + Args: + secret_spec: Secret specification dictionary + rayjob_result: The result from RayJob creation containing UID + + Returns: + str: Name of the created Secret + """ + + secret_name = secret_spec["metadata"]["name"] + + metadata = client.V1ObjectMeta(**secret_spec["metadata"]) + + # Add owner reference to ensure proper cleanup + # We can trust that rayjob_result contains UID since submit_job() only returns + # complete K8s resources or None, and we already validated result exists + logger.info( + f"Adding owner reference to Secret '{secret_name}' with RayJob UID: {rayjob_result['metadata']['uid']}" + ) + metadata.owner_references = [ + client.V1OwnerReference( + api_version="ray.io/v1", + kind="RayJob", + name=job.name, + uid=rayjob_result["metadata"]["uid"], + controller=True, + block_owner_deletion=True, + ) + ] + + # Convert dict spec to V1Secret + # Use stringData instead of data to avoid double base64 encoding + # Our zip files are already base64-encoded, so stringData will handle the final encoding + secret = client.V1Secret( + metadata=metadata, + type=secret_spec.get("type", "Opaque"), + string_data=secret_spec["data"], + ) + + # Create Secret via Kubernetes API + k8s_api = client.CoreV1Api(get_api_client()) + try: + k8s_api.create_namespaced_secret(namespace=job.namespace, body=secret) + logger.info( + f"Created Secret '{secret_name}' with {len(secret_spec['data'])} files" + ) + except client.ApiException as e: + if e.status == 409: # Already exists + logger.info(f"Secret '{secret_name}' already exists, updating...") + k8s_api.replace_namespaced_secret( + name=secret_name, namespace=job.namespace, body=secret + ) + else: + raise RuntimeError(f"Failed to create Secret '{secret_name}': {e}") + + return secret_name + + +def create_file_secret( + job: RayJob, files: Dict[str, str], rayjob_result: Dict[str, Any] +): + """ + Create Secret with owner reference for local files. + """ + # Use a basic config builder for Secret creation + config_builder = ManagedClusterConfig() + + # Filter out metadata keys (like __entrypoint_path__) from Secret data + secret_files = {k: v for k, v in files.items() if not k.startswith("__")} + + # Validate and build Secret spec + config_builder.validate_secret_size(secret_files) + secret_spec = config_builder.build_file_secret_spec( + job_name=job.name, namespace=job.namespace, files=secret_files + ) + + # Create Secret with owner reference + create_secret_from_spec(job, secret_spec, rayjob_result) diff --git a/src/codeflare_sdk/ray/rayjobs/status.py b/src/codeflare_sdk/ray/rayjobs/status.py new file mode 100644 index 00000000..027ed09c --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/status.py @@ -0,0 +1,64 @@ +# Copyright 2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The status sub-module defines Enums containing information for Ray job +deployment states and CodeFlare job states, as well as +dataclasses to store information for Ray jobs. +""" + +from dataclasses import dataclass +from enum import Enum +from typing import Optional + + +class RayJobDeploymentStatus(Enum): + """ + Defines the possible deployment states of a Ray job (from the KubeRay RayJob API). + """ + + COMPLETE = "Complete" + RUNNING = "Running" + FAILED = "Failed" + SUSPENDED = "Suspended" + UNKNOWN = "Unknown" + + +class CodeflareRayJobStatus(Enum): + """ + Defines the possible reportable states of a CodeFlare Ray job. + """ + + COMPLETE = 1 + RUNNING = 2 + FAILED = 3 + SUSPENDED = 4 + UNKNOWN = 5 + + +@dataclass +class RayJobInfo: + """ + For storing information about a Ray job. + """ + + name: str + job_id: str + status: RayJobDeploymentStatus + namespace: str + cluster_name: str + start_time: Optional[str] = None + end_time: Optional[str] = None + failed_attempts: int = 0 + succeeded_attempts: int = 0 diff --git a/src/codeflare_sdk/ray/rayjobs/test/conftest.py b/src/codeflare_sdk/ray/rayjobs/test/conftest.py new file mode 100644 index 00000000..bad195a7 --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/test/conftest.py @@ -0,0 +1,45 @@ +"""Shared pytest fixtures for rayjobs tests.""" + +import pytest +from unittest.mock import MagicMock + + +# Global test setup that runs automatically for ALL tests +@pytest.fixture(autouse=True) +def auto_mock_setup(mocker): + """Automatically mock common dependencies for all tests.""" + mocker.patch("kubernetes.config.load_kube_config") + + # Always mock get_default_kueue_name to prevent K8s API calls + mocker.patch( + "codeflare_sdk.ray.rayjobs.rayjob.get_default_kueue_name", + return_value="default-queue", + ) + + mock_get_ns = mocker.patch( + "codeflare_sdk.ray.rayjobs.rayjob.get_current_namespace", + return_value="test-namespace", + ) + + mock_rayjob_api = mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayjobApi") + mock_rayjob_instance = MagicMock() + mock_rayjob_api.return_value = mock_rayjob_instance + + mock_cluster_api = mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayClusterApi") + mock_cluster_instance = MagicMock() + mock_cluster_api.return_value = mock_cluster_instance + + mock_k8s_api = mocker.patch("kubernetes.client.CoreV1Api") + mock_k8s_instance = MagicMock() + mock_k8s_api.return_value = mock_k8s_instance + + # Mock get_api_client in runtime_env module where it's actually used + mocker.patch("codeflare_sdk.ray.rayjobs.runtime_env.get_api_client") + + # Return the mocked instances so tests can configure them as needed + return { + "rayjob_api": mock_rayjob_instance, + "cluster_api": mock_cluster_instance, + "k8s_api": mock_k8s_instance, + "get_current_namespace": mock_get_ns, + } diff --git a/src/codeflare_sdk/ray/rayjobs/test/test_config.py b/src/codeflare_sdk/ray/rayjobs/test/test_config.py new file mode 100644 index 00000000..182ff90c --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/test/test_config.py @@ -0,0 +1,346 @@ +""" +Tests for the simplified ManagedClusterConfig accelerator_configs behavior. +""" + +import pytest +from codeflare_sdk.ray.rayjobs.config import ManagedClusterConfig, DEFAULT_ACCELERATORS +from kubernetes.client import V1VolumeMount +from kubernetes.client import V1Volume, V1SecretVolumeSource + + +def test_accelerator_configs_defaults_to_default_accelerators(): + """Test that accelerator_configs defaults to DEFAULT_ACCELERATORS.copy()""" + config = ManagedClusterConfig() + + # Should have all the default accelerators + assert "nvidia.com/gpu" in config.accelerator_configs + assert "intel.com/gpu" in config.accelerator_configs + assert "google.com/tpu" in config.accelerator_configs + + # Should be a copy, not the same object + assert config.accelerator_configs is not DEFAULT_ACCELERATORS + assert config.accelerator_configs == DEFAULT_ACCELERATORS + + +def test_accelerator_configs_can_be_overridden(): + """Test that users can override accelerator_configs with custom mappings""" + custom_configs = { + "nvidia.com/gpu": "GPU", + "custom.com/accelerator": "CUSTOM_ACCELERATOR", + } + + config = ManagedClusterConfig(accelerator_configs=custom_configs) + + # Should have custom configs + assert config.accelerator_configs == custom_configs + assert "custom.com/accelerator" in config.accelerator_configs + assert "nvidia.com/gpu" in config.accelerator_configs + + # Should NOT have other defaults + assert "intel.com/gpu" not in config.accelerator_configs + assert "google.com/tpu" not in config.accelerator_configs + + +def test_accelerator_configs_can_extend_defaults(): + """Test that users can extend defaults by providing additional configs""" + extended_configs = { + **DEFAULT_ACCELERATORS, + "custom.com/accelerator": "CUSTOM_ACCEL", + } + + config = ManagedClusterConfig(accelerator_configs=extended_configs) + + # Should have all defaults plus custom + assert "nvidia.com/gpu" in config.accelerator_configs + assert "intel.com/gpu" in config.accelerator_configs + assert "custom.com/accelerator" in config.accelerator_configs + assert config.accelerator_configs["custom.com/accelerator"] == "CUSTOM_ACCEL" + + +def test_gpu_validation_works_with_defaults(): + """Test that GPU validation works with default accelerator configs""" + config = ManagedClusterConfig(head_accelerators={"nvidia.com/gpu": 1}) + + # Should not raise any errors + assert config.head_accelerators == {"nvidia.com/gpu": 1} + + +def test_gpu_validation_works_with_custom_configs(): + """Test that GPU validation works with custom accelerator configs""" + config = ManagedClusterConfig( + accelerator_configs={"custom.com/accelerator": "CUSTOM_ACCEL"}, + head_accelerators={"custom.com/accelerator": 1}, + ) + + # Should not raise any errors + assert config.head_accelerators == {"custom.com/accelerator": 1} + + +def test_gpu_validation_fails_with_unsupported_accelerator(): + """Test that GPU validation fails with unsupported accelerators""" + with pytest.raises( + ValueError, match="GPU configuration 'unsupported.com/accelerator' not found" + ): + ManagedClusterConfig(head_accelerators={"unsupported.com/accelerator": 1}) + + +def test_config_type_validation_errors(mocker): + """Test that type validation properly raises errors with incorrect types.""" + # Mock the _is_type method to return False for type checking + mocker.patch.object( + ManagedClusterConfig, + "_is_type", + side_effect=lambda value, expected_type: False, # Always fail type check + ) + + # This should raise TypeError during initialization + with pytest.raises(TypeError, match="Type validation failed"): + ManagedClusterConfig() + + +def test_config_is_type_method(): + """Test the _is_type static method for type checking.""" + # Test basic types + assert ManagedClusterConfig._is_type("test", str) is True + assert ManagedClusterConfig._is_type(123, int) is True + assert ManagedClusterConfig._is_type(123, str) is False + + # Test optional types (Union with None) + from typing import Optional + + assert ManagedClusterConfig._is_type(None, Optional[str]) is True + assert ManagedClusterConfig._is_type("test", Optional[str]) is True + assert ManagedClusterConfig._is_type(123, Optional[str]) is False + + # Test dict types + assert ManagedClusterConfig._is_type({}, dict) is True + assert ManagedClusterConfig._is_type({"key": "value"}, dict) is True + assert ManagedClusterConfig._is_type([], dict) is False + + +def test_ray_usage_stats_always_disabled_by_default(): + """Test that RAY_USAGE_STATS_ENABLED is always set to '0' by default""" + config = ManagedClusterConfig() + + # Should always have the environment variable set to "0" + assert "RAY_USAGE_STATS_ENABLED" in config.envs + assert config.envs["RAY_USAGE_STATS_ENABLED"] == "0" + + +def test_ray_usage_stats_overwrites_user_env(): + """Test that RAY_USAGE_STATS_ENABLED is always set to '0' even if user specifies it""" + # User tries to enable usage stats + config = ManagedClusterConfig(envs={"RAY_USAGE_STATS_ENABLED": "1"}) + + # Should still be disabled (our setting takes precedence) + assert "RAY_USAGE_STATS_ENABLED" in config.envs + assert config.envs["RAY_USAGE_STATS_ENABLED"] == "0" + + +def test_ray_usage_stats_overwrites_user_env_string(): + """Test that RAY_USAGE_STATS_ENABLED is always set to '0' even if user specifies it as string""" + # User tries to enable usage stats with string + config = ManagedClusterConfig(envs={"RAY_USAGE_STATS_ENABLED": "true"}) + + # Should still be disabled (our setting takes precedence) + assert "RAY_USAGE_STATS_ENABLED" in config.envs + assert config.envs["RAY_USAGE_STATS_ENABLED"] == "0" + + +def test_ray_usage_stats_with_other_user_envs(): + """Test that RAY_USAGE_STATS_ENABLED is set correctly while preserving other user envs""" + # User sets other environment variables + user_envs = { + "CUSTOM_VAR": "custom_value", + "ANOTHER_VAR": "another_value", + "RAY_USAGE_STATS_ENABLED": "1", # This should be overwritten + } + + config = ManagedClusterConfig(envs=user_envs) + + # Our setting should take precedence + assert config.envs["RAY_USAGE_STATS_ENABLED"] == "0" + + # Other user envs should be preserved + assert config.envs["CUSTOM_VAR"] == "custom_value" + assert config.envs["ANOTHER_VAR"] == "another_value" + + # Total count should be correct (3 user envs) + assert len(config.envs) == 3 + + +def test_add_file_volumes_existing_volume_early_return(): + """Test add_file_volumes early return when volume already exists.""" + + config = ManagedClusterConfig() + + # Pre-add a volume with same name + existing_volume = V1Volume( + name="ray-job-files", + secret=V1SecretVolumeSource(secret_name="existing-files"), + ) + config.volumes.append(existing_volume) + + # Should return early and not add duplicate + config.add_file_volumes(secret_name="new-files") + + # Should still have only one volume, no mount added + assert len(config.volumes) == 1 + assert len(config.volume_mounts) == 0 + + +def test_add_file_volumes_existing_mount_early_return(): + """Test add_file_volumes early return when mount already exists.""" + + config = ManagedClusterConfig() + + # Pre-add a mount with same name + existing_mount = V1VolumeMount(name="ray-job-files", mount_path="/existing/path") + config.volume_mounts.append(existing_mount) + + # Should return early and not add duplicate + config.add_file_volumes(secret_name="new-files") + + # Should still have only one mount, no volume added + assert len(config.volumes) == 0 + assert len(config.volume_mounts) == 1 + + +def test_build_file_secret_spec_labels(): + """Test that build_file_secret_spec creates Secret with correct labels.""" + config = ManagedClusterConfig() + + job_name = "test-job" + namespace = "test-namespace" + files = {"test.py": "print('hello')", "helper.py": "# helper code"} + + secret_spec = config.build_file_secret_spec(job_name, namespace, files) + + assert secret_spec["apiVersion"] == "v1" + assert secret_spec["kind"] == "Secret" + assert secret_spec["type"] == "Opaque" + assert secret_spec["metadata"]["name"] == f"{job_name}-files" + assert secret_spec["metadata"]["namespace"] == namespace + + labels = secret_spec["metadata"]["labels"] + assert labels["ray.io/job-name"] == job_name + assert labels["app.kubernetes.io/managed-by"] == "codeflare-sdk" + assert labels["app.kubernetes.io/component"] == "rayjob-files" + + assert secret_spec["data"] == files + + +def test_managed_cluster_config_uses_update_image_for_head(mocker): + """Test that ManagedClusterConfig calls update_image() for head container.""" + # Mock update_image where it's used (in config module), not where it's defined + mock_update_image = mocker.patch( + "codeflare_sdk.ray.rayjobs.config.update_image", + return_value="mocked-image:latest", + ) + + config = ManagedClusterConfig(image="custom-image:v1") + + # Build cluster spec (which should call update_image) + spec = config.build_ray_cluster_spec("test-cluster") + + # Verify update_image was called for head container + assert mock_update_image.called + # Verify head container has the mocked image + head_container = spec["headGroupSpec"]["template"].spec.containers[0] + assert head_container.image == "mocked-image:latest" + + +def test_managed_cluster_config_uses_update_image_for_worker(mocker): + """Test that ManagedClusterConfig calls update_image() for worker container.""" + # Mock update_image where it's used (in config module), not where it's defined + mock_update_image = mocker.patch( + "codeflare_sdk.ray.rayjobs.config.update_image", + return_value="mocked-image:latest", + ) + + config = ManagedClusterConfig(image="custom-image:v1") + + # Build cluster spec (which should call update_image) + spec = config.build_ray_cluster_spec("test-cluster") + + # Verify update_image was called for worker container + assert mock_update_image.called + # Verify worker container has the mocked image + worker_container = spec["workerGroupSpecs"][0]["template"].spec.containers[0] + assert worker_container.image == "mocked-image:latest" + + +def test_managed_cluster_config_with_empty_image_uses_update_image(mocker): + """Test that empty image triggers update_image() to auto-detect.""" + # Mock update_image where it's used (in config module), not where it's defined + mock_update_image = mocker.patch( + "codeflare_sdk.ray.rayjobs.config.update_image", + return_value="auto-detected-image:py3.12", + ) + + config = ManagedClusterConfig(image="") + + # Build cluster spec + spec = config.build_ray_cluster_spec("test-cluster") + + # Verify update_image was called with empty string + mock_update_image.assert_called_with("") + + # Verify containers have the auto-detected image + head_container = spec["headGroupSpec"]["template"].spec.containers[0] + assert head_container.image == "auto-detected-image:py3.12" + + worker_container = spec["workerGroupSpecs"][0]["template"].spec.containers[0] + assert worker_container.image == "auto-detected-image:py3.12" + + +def test_build_ray_cluster_spec_has_enable_in_tree_autoscaling_false(): + """Test that build_ray_cluster_spec sets enableInTreeAutoscaling to False.""" + config = ManagedClusterConfig() + + spec = config.build_ray_cluster_spec("test-cluster") + + # Verify enableInTreeAutoscaling is set to False (required for Kueue) + assert "enableInTreeAutoscaling" in spec + assert spec["enableInTreeAutoscaling"] is False + + +def test_build_ray_cluster_spec_autoscaling_disabled_for_kueue(): + """Test that autoscaling is explicitly disabled for Kueue-managed jobs.""" + config = ManagedClusterConfig(num_workers=3) + + spec = config.build_ray_cluster_spec("kueue-cluster") + + # Verify enableInTreeAutoscaling is False + assert spec["enableInTreeAutoscaling"] is False + + # Verify worker replicas are fixed (min == max == replicas) + worker_spec = spec["workerGroupSpecs"][0] + assert worker_spec["replicas"] == 3 + assert worker_spec["minReplicas"] == 3 + assert worker_spec["maxReplicas"] == 3 + + +def test_managed_cluster_config_default_image_integration(): + """Test that ManagedClusterConfig works with default images (integration test).""" + # Create config without specifying an image (should auto-detect based on Python version) + config = ManagedClusterConfig() + + # Build cluster spec + spec = config.build_ray_cluster_spec("test-cluster") + + # Verify head container has an image (should be auto-detected) + head_container = spec["headGroupSpec"]["template"].spec.containers[0] + assert head_container.image is not None + assert len(head_container.image) > 0 + # Should be one of the supported images + from codeflare_sdk.common.utils.constants import ( + CUDA_PY311_RUNTIME_IMAGE, + CUDA_PY312_RUNTIME_IMAGE, + ) + + assert head_container.image in [CUDA_PY311_RUNTIME_IMAGE, CUDA_PY312_RUNTIME_IMAGE] + + # Verify worker container has the same image + worker_container = spec["workerGroupSpecs"][0]["template"].spec.containers[0] + assert worker_container.image == head_container.image diff --git a/src/codeflare_sdk/ray/rayjobs/test/test_pretty_print.py b/src/codeflare_sdk/ray/rayjobs/test/test_pretty_print.py new file mode 100644 index 00000000..3bbe8bee --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/test/test_pretty_print.py @@ -0,0 +1,265 @@ +# Copyright 2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from codeflare_sdk.ray.rayjobs.pretty_print import ( + _get_status_display, + print_job_status, + print_no_job_found, +) +from codeflare_sdk.ray.rayjobs.status import RayJobDeploymentStatus, RayJobInfo +from unittest.mock import MagicMock, call + + +def test_get_status_display(): + """ + Test the _get_status_display function. + """ + # Test Complete status + display, color = _get_status_display(RayJobDeploymentStatus.COMPLETE) + assert display == "Complete :white_heavy_check_mark:" + assert color == "[white on green][bold]Name" + + # Test Running status + display, color = _get_status_display(RayJobDeploymentStatus.RUNNING) + assert display == "Running :gear:" + assert color == "[white on blue][bold]Name" + + # Test Failed status + display, color = _get_status_display(RayJobDeploymentStatus.FAILED) + assert display == "Failed :x:" + assert color == "[white on red][bold]Name" + + # Test Suspended status + display, color = _get_status_display(RayJobDeploymentStatus.SUSPENDED) + assert display == "Suspended :pause_button:" + assert color == "[white on yellow][bold]Name" + + # Test Unknown status + display, color = _get_status_display(RayJobDeploymentStatus.UNKNOWN) + assert display == "Unknown :question:" + assert color == "[white on red][bold]Name" + + +def test_print_job_status_running_format(mocker): + """ + Test the print_job_status function format for a running job. + """ + # Mock Rich components to verify format + mock_console = MagicMock() + mock_inner_table = MagicMock() + mock_main_table = MagicMock() + mock_panel = MagicMock() + + # Mock Table to return different instances for inner and main tables + table_instances = [mock_inner_table, mock_main_table] + mock_table_class = MagicMock(side_effect=table_instances) + + mocker.patch( + "codeflare_sdk.ray.rayjobs.pretty_print.Console", return_value=mock_console + ) + mocker.patch("codeflare_sdk.ray.rayjobs.pretty_print.Table", mock_table_class) + mocker.patch("codeflare_sdk.ray.rayjobs.pretty_print.Panel", mock_panel) + + # Create test job info for running job + job_info = RayJobInfo( + name="test-job", + job_id="test-job-abc123", + status=RayJobDeploymentStatus.RUNNING, + namespace="test-ns", + cluster_name="test-cluster", + start_time="2025-07-28T11:37:07Z", + failed_attempts=0, + succeeded_attempts=0, + ) + + # Call the function + print_job_status(job_info) + + # Verify both Table calls + expected_table_calls = [ + call(box=None, show_header=False), # Inner content table + call( + box=None, title="[bold] :package: CodeFlare RayJob Status :package:" + ), # Main wrapper table + ] + mock_table_class.assert_has_calls(expected_table_calls) + + # Verify inner table rows are added in correct order and format (versus our hard-coded version of this for cluster) + expected_calls = [ + call("[white on blue][bold]Name"), # Header with blue color for running + call( + "[bold underline]test-job", "Running :gear:" + ), # Name and status with gear emoji + call(), # Empty separator row + call("[bold]Job ID:[/bold] test-job-abc123"), + call("[bold]Status:[/bold] Running"), + call("[bold]RayCluster:[/bold] test-cluster"), + call("[bold]Namespace:[/bold] test-ns"), + call(), # Empty row before timing info + call("[bold]Started:[/bold] 2025-07-28T11:37:07Z"), + ] + mock_inner_table.add_row.assert_has_calls(expected_calls) + + # Verify Panel is created with inner table + mock_panel.fit.assert_called_once_with(mock_inner_table) + + # Verify main table gets the panel + mock_main_table.add_row.assert_called_once_with(mock_panel.fit.return_value) + + # Verify console prints the main table + mock_console.print.assert_called_once_with(mock_main_table) + + +def test_print_job_status_complete_format(mocker): + """ + Test the print_job_status function format for a completed job. + """ + # Mock Rich components + mock_console = MagicMock() + mock_inner_table = MagicMock() + mock_main_table = MagicMock() + mock_panel = MagicMock() + + # Mock Table to return different instances + table_instances = [mock_inner_table, mock_main_table] + mock_table_class = MagicMock(side_effect=table_instances) + + mocker.patch( + "codeflare_sdk.ray.rayjobs.pretty_print.Console", return_value=mock_console + ) + mocker.patch("codeflare_sdk.ray.rayjobs.pretty_print.Table", mock_table_class) + mocker.patch("codeflare_sdk.ray.rayjobs.pretty_print.Panel", mock_panel) + + # Create test job info for completed job + job_info = RayJobInfo( + name="completed-job", + job_id="completed-job-xyz789", + status=RayJobDeploymentStatus.COMPLETE, + namespace="prod-ns", + cluster_name="prod-cluster", + start_time="2025-07-28T11:37:07Z", + failed_attempts=0, + succeeded_attempts=1, + ) + + # Call the function + print_job_status(job_info) + + # Verify correct header color for completed job (green) (versus our hard-coded version of this for cluster) + expected_calls = [ + call("[white on green][bold]Name"), # Green header for complete + call( + "[bold underline]completed-job", "Complete :white_heavy_check_mark:" + ), # Checkmark emoji + call(), # Empty separator + call("[bold]Job ID:[/bold] completed-job-xyz789"), + call("[bold]Status:[/bold] Complete"), + call("[bold]RayCluster:[/bold] prod-cluster"), + call("[bold]Namespace:[/bold] prod-ns"), + call(), # Empty row before timing info + call("[bold]Started:[/bold] 2025-07-28T11:37:07Z"), + ] + mock_inner_table.add_row.assert_has_calls(expected_calls) + + +def test_print_job_status_failed_with_attempts_format(mocker): + """ + Test the print_job_status function format for a failed job with attempts. + """ + # Mock Rich components + mock_console = MagicMock() + mock_inner_table = MagicMock() + mock_main_table = MagicMock() + mock_panel = MagicMock() + + # Mock Table to return different instances + table_instances = [mock_inner_table, mock_main_table] + mock_table_class = MagicMock(side_effect=table_instances) + + mocker.patch( + "codeflare_sdk.ray.rayjobs.pretty_print.Console", return_value=mock_console + ) + mocker.patch("codeflare_sdk.ray.rayjobs.pretty_print.Table", mock_table_class) + mocker.patch("codeflare_sdk.ray.rayjobs.pretty_print.Panel", mock_panel) + + # Create test job info with failures + job_info = RayJobInfo( + name="failing-job", + job_id="failing-job-fail123", + status=RayJobDeploymentStatus.FAILED, + namespace="test-ns", + cluster_name="test-cluster", + start_time="2025-07-28T11:37:07Z", + failed_attempts=3, # Has failures + succeeded_attempts=0, + ) + + # Call the function + print_job_status(job_info) + + # Verify correct formatting including failure attempts (versus our hard-coded version of this for cluster) + expected_calls = [ + call("[white on red][bold]Name"), # Red header for failed + call("[bold underline]failing-job", "Failed :x:"), # X emoji for failed + call(), # Empty separator + call("[bold]Job ID:[/bold] failing-job-fail123"), + call("[bold]Status:[/bold] Failed"), + call("[bold]RayCluster:[/bold] test-cluster"), + call("[bold]Namespace:[/bold] test-ns"), + call(), # Empty row before timing info + call("[bold]Started:[/bold] 2025-07-28T11:37:07Z"), + call("[bold]Failed Attempts:[/bold] 3"), # Failed attempts should be shown + ] + mock_inner_table.add_row.assert_has_calls(expected_calls) + + +def test_print_no_job_found_format(mocker): + """ + Test the print_no_job_found function format. + """ + # Mock Rich components + mock_console = MagicMock() + mock_inner_table = MagicMock() + mock_main_table = MagicMock() + mock_panel = MagicMock() + + # Mock Table to return different instances + table_instances = [mock_inner_table, mock_main_table] + mock_table_class = MagicMock(side_effect=table_instances) + + mocker.patch( + "codeflare_sdk.ray.rayjobs.pretty_print.Console", return_value=mock_console + ) + mocker.patch("codeflare_sdk.ray.rayjobs.pretty_print.Table", mock_table_class) + mocker.patch("codeflare_sdk.ray.rayjobs.pretty_print.Panel", mock_panel) + + # Call the function + print_no_job_found("missing-job", "test-namespace") + + # Verify error message format (versus our hard-coded version of this for cluster) + expected_calls = [ + call("[white on red][bold]Name"), # Red header for error + call( + "[bold underline]missing-job", "[bold red]No RayJob found" + ), # Error message in red + call(), # Empty separator + call(), # Another empty row + call("Please run rayjob.submit() to submit a job."), # Helpful hint + call(), # Empty separator + call("[bold]Namespace:[/bold] test-namespace"), + ] + mock_inner_table.add_row.assert_has_calls(expected_calls) + + # Verify Panel is used + mock_panel.fit.assert_called_once_with(mock_inner_table) diff --git a/src/codeflare_sdk/ray/rayjobs/test/test_rayjob.py b/src/codeflare_sdk/ray/rayjobs/test/test_rayjob.py new file mode 100644 index 00000000..928cc1f8 --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/test/test_rayjob.py @@ -0,0 +1,1783 @@ +# Copyright 2022-2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from unittest.mock import MagicMock +from codeflare_sdk.common.utils.constants import RAY_VERSION +from ray.runtime_env import RuntimeEnv + +from codeflare_sdk.ray.rayjobs.rayjob import RayJob +from codeflare_sdk.ray.cluster.config import ClusterConfiguration +from codeflare_sdk.ray.rayjobs.config import ManagedClusterConfig +from kubernetes.client import V1Volume, V1VolumeMount, V1Toleration + + +def test_rayjob_submit_success(auto_mock_setup): + """ + Test successful RayJob submission. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + + mock_api_instance.submit.return_value = {"metadata": {"name": "test-rayjob"}} + + rayjob = RayJob( + job_name="test-rayjob", + cluster_name="test-ray-cluster", + namespace="test-namespace", + entrypoint="python -c 'print(\"hello world\")'", + runtime_env=RuntimeEnv(pip=["requests"]), + ) + + job_id = rayjob.submit() + + assert job_id == "test-rayjob" + + mock_api_instance.submit_job.assert_called_once() + call_args = mock_api_instance.submit_job.call_args + + assert call_args.kwargs["k8s_namespace"] == "test-namespace" + + job_cr = call_args.kwargs["job"] + assert job_cr["metadata"]["name"] == "test-rayjob" + assert job_cr["metadata"]["namespace"] == "test-namespace" + assert job_cr["spec"]["entrypoint"] == "python -c 'print(\"hello world\")'" + assert job_cr["spec"]["clusterSelector"]["ray.io/cluster"] == "test-ray-cluster" + assert job_cr["spec"]["runtimeEnvYAML"] == "pip:\n- requests\n" + + +def test_rayjob_submit_failure(auto_mock_setup): + """ + Test RayJob submission failure. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + + mock_api_instance.submit_job.return_value = None + + rayjob = RayJob( + job_name="test-rayjob", + cluster_name="test-ray-cluster", + namespace="default", + entrypoint="python -c 'print()'", + runtime_env=RuntimeEnv(pip=["numpy"]), + ) + + with pytest.raises(RuntimeError, match="Failed to submit RayJob test-rayjob"): + rayjob.submit() + + +def test_rayjob_init_validation_both_provided(auto_mock_setup): + """ + Test that providing both cluster_name and cluster_config raises error. + """ + cluster_config = ClusterConfiguration(name="test-cluster", namespace="test") + + with pytest.raises( + ValueError, + match="❌ Configuration Error: You cannot specify both 'cluster_name' and 'cluster_config'", + ): + RayJob( + job_name="test-job", + cluster_name="existing-cluster", + cluster_config=cluster_config, + entrypoint="python -c 'print()'", + ) + + +def test_rayjob_init_validation_neither_provided(auto_mock_setup): + """ + Test that providing neither cluster_name nor cluster_config raises error. + """ + with pytest.raises( + ValueError, + match="❌ Configuration Error: You must provide either 'cluster_name'", + ): + RayJob(job_name="test-job", entrypoint="python test.py") + + +def test_rayjob_init_with_cluster_config(auto_mock_setup): + """ + Test RayJob initialization with cluster configuration for auto-creation. + """ + cluster_config = ClusterConfiguration( + name="auto-cluster", namespace="test-namespace", num_workers=2 + ) + + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python -c 'print()'", + namespace="test-namespace", + ) + + assert rayjob.name == "test-job" + assert rayjob.cluster_name == "test-job-cluster" # Generated from job name + assert rayjob._cluster_config == cluster_config + assert rayjob._cluster_name is None + + +def test_rayjob_cluster_name_generation(auto_mock_setup): + """ + Test that cluster names are generated when config has empty name. + """ + cluster_config = ClusterConfiguration( + name="", # Empty name should trigger generation + namespace="test-namespace", + num_workers=1, + ) + + rayjob = RayJob( + job_name="my-job", + cluster_config=cluster_config, + entrypoint="python -c 'print()'", + namespace="test-namespace", + ) + + assert rayjob.cluster_name == "my-job-cluster" + + +def test_rayjob_cluster_config_namespace_none(auto_mock_setup): + """ + Test that cluster config namespace is set when None. + """ + cluster_config = ClusterConfiguration( + name="test-cluster", + namespace=None, # This should be set to job namespace + num_workers=1, + ) + + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + namespace="job-namespace", + entrypoint="python -c 'print()'", + ) + + assert rayjob.namespace == "job-namespace" + + +def test_rayjob_with_active_deadline_seconds(auto_mock_setup): + """ + Test RayJob CR generation with active deadline seconds. + """ + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + namespace="test-namespace", + entrypoint="python main.py", + active_deadline_seconds=30, + ) + + rayjob_cr = rayjob._build_rayjob_cr() + + assert rayjob_cr["spec"]["activeDeadlineSeconds"] == 30 + + +def test_build_ray_cluster_spec_no_config_error(auto_mock_setup): + """ + Test _build_ray_cluster_spec raises error when no cluster config. + """ + rayjob = RayJob( + job_name="test-job", + cluster_name="existing-cluster", + entrypoint="python -c 'print()'", + namespace="test-namespace", + ) + + rayjob_cr = rayjob._build_rayjob_cr() + + assert rayjob_cr["spec"]["clusterSelector"]["ray.io/cluster"] == "existing-cluster" + assert "rayClusterSpec" not in rayjob_cr["spec"] + + +def test_build_ray_cluster_spec(mocker, auto_mock_setup): + """ + Test _build_ray_cluster_spec method. + """ + + mock_ray_cluster = { + "apiVersion": "ray.io/v1", + "kind": "RayCluster", + "metadata": {"name": "test-cluster", "namespace": "test"}, + "spec": { + "rayVersion": RAY_VERSION, + "headGroupSpec": {"replicas": 1}, + "workerGroupSpecs": [{"replicas": 2}], + }, + } + cluster_config = ManagedClusterConfig(num_workers=2) + mocker.patch.object( + cluster_config, "build_ray_cluster_spec", return_value=mock_ray_cluster["spec"] + ) + + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python -c 'print()'", + namespace="test-namespace", + ) + + rayjob_cr = rayjob._build_rayjob_cr() + + assert "rayClusterSpec" in rayjob_cr["spec"] + cluster_config.build_ray_cluster_spec.assert_called_once_with( + cluster_name="test-job-cluster" + ) + + +def test_build_rayjob_cr_with_existing_cluster(auto_mock_setup): + """ + Test _build_rayjob_cr method with existing cluster. + """ + + rayjob = RayJob( + job_name="test-job", + cluster_name="existing-cluster", + namespace="test-namespace", + entrypoint="python main.py", + ttl_seconds_after_finished=300, + ) + + rayjob_cr = rayjob._build_rayjob_cr() + + assert rayjob_cr["apiVersion"] == "ray.io/v1" + assert rayjob_cr["kind"] == "RayJob" + assert rayjob_cr["metadata"]["name"] == "test-job" + spec = rayjob_cr["spec"] + assert spec["entrypoint"] == "python main.py" + assert spec["shutdownAfterJobFinishes"] is False + assert spec["ttlSecondsAfterFinished"] == 300 + + assert spec["clusterSelector"]["ray.io/cluster"] == "existing-cluster" + assert "rayClusterSpec" not in spec + + +def test_build_rayjob_cr_with_auto_cluster(mocker, auto_mock_setup): + """ + Test _build_rayjob_cr method with auto-created cluster. + """ + mock_ray_cluster = { + "apiVersion": "ray.io/v1", + "kind": "RayCluster", + "metadata": {"name": "auto-cluster", "namespace": "test"}, + "spec": { + "rayVersion": RAY_VERSION, + "headGroupSpec": {"replicas": 1}, + "workerGroupSpecs": [{"replicas": 2}], + }, + } + cluster_config = ManagedClusterConfig(num_workers=2) + + mocker.patch.object( + cluster_config, "build_ray_cluster_spec", return_value=mock_ray_cluster["spec"] + ) + + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python main.py", + namespace="test-namespace", + ) + + rayjob_cr = rayjob._build_rayjob_cr() + assert rayjob_cr["spec"]["rayClusterSpec"] == mock_ray_cluster["spec"] + assert "clusterSelector" not in rayjob_cr["spec"] + + +def test_submit_validation_no_entrypoint(auto_mock_setup): + """ + Test that submit() raises error when entrypoint is None. + """ + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint=None, # No entrypoint provided + namespace="test-namespace", + ) + + with pytest.raises( + ValueError, match="Entrypoint must be provided to submit a RayJob" + ): + rayjob.submit() + + +def test_submit_with_auto_cluster(mocker, auto_mock_setup): + """ + Test successful submission with auto-created cluster. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + + mock_ray_cluster = { + "apiVersion": "ray.io/v1", + "kind": "RayCluster", + "spec": { + "rayVersion": RAY_VERSION, + "headGroupSpec": {"replicas": 1}, + "workerGroupSpecs": [{"replicas": 1}], + }, + } + mock_api_instance.submit_job.return_value = True + + cluster_config = ManagedClusterConfig(num_workers=1) + mocker.patch.object( + cluster_config, "build_ray_cluster_spec", return_value=mock_ray_cluster["spec"] + ) + + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python -c 'print()'", + namespace="test-namespace", + ) + + result = rayjob.submit() + + assert result == "test-job" + + mock_api_instance.submit_job.assert_called_once() + call_args = mock_api_instance.submit_job.call_args + + job_cr = call_args.kwargs["job"] + assert "rayClusterSpec" in job_cr["spec"] + assert job_cr["spec"]["rayClusterSpec"] == mock_ray_cluster["spec"] + + +def test_namespace_auto_detection_success(auto_mock_setup): + """ + Test successful namespace auto-detection. + """ + auto_mock_setup["get_current_namespace"].return_value = "detected-ns" + + rayjob = RayJob( + job_name="test-job", entrypoint="python test.py", cluster_name="test-cluster" + ) + + assert rayjob.namespace == "detected-ns" + + +def test_namespace_auto_detection_fallback(auto_mock_setup): + """ + Test that namespace auto-detection failure raises an error. + """ + auto_mock_setup["get_current_namespace"].return_value = None + + with pytest.raises(ValueError, match="Could not auto-detect Kubernetes namespace"): + RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + cluster_name="test-cluster", + ) + + +def test_namespace_explicit_override(auto_mock_setup): + """ + Test that explicit namespace overrides auto-detection. + """ + auto_mock_setup["get_current_namespace"].return_value = "detected-ns" + + rayjob = RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + cluster_name="test-cluster", + namespace="explicit-ns", + ) + + assert rayjob.namespace == "explicit-ns" + + +def test_rayjob_with_rayjob_cluster_config(auto_mock_setup): + """ + Test RayJob with the new ManagedClusterConfig. + """ + cluster_config = ManagedClusterConfig( + num_workers=2, + head_cpu_requests="500m", + head_memory_requests="512Mi", + ) + + rayjob = RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + cluster_config=cluster_config, + namespace="test-namespace", + ) + + assert rayjob._cluster_config == cluster_config + assert rayjob.cluster_name == "test-job-cluster" # Generated from job name + + +def test_rayjob_cluster_config_validation(auto_mock_setup): + """ + Test validation of ManagedClusterConfig parameters. + """ + cluster_config = ManagedClusterConfig() + + rayjob = RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + cluster_config=cluster_config, + namespace="test-namespace", + ) + + assert rayjob._cluster_config is not None + + +def test_rayjob_missing_entrypoint_validation(auto_mock_setup): + """ + Test that RayJob requires entrypoint for submission. + """ + with pytest.raises( + TypeError, match="missing 1 required positional argument: 'entrypoint'" + ): + RayJob( + job_name="test-job", + cluster_name="test-cluster", + ) + + +def test_build_ray_cluster_spec_integration(mocker, auto_mock_setup): + """ + Test integration with the new build_ray_cluster_spec method. + """ + cluster_config = ManagedClusterConfig() + mock_spec = {"spec": "test-spec"} + mocker.patch.object( + cluster_config, "build_ray_cluster_spec", return_value=mock_spec + ) + + rayjob = RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + cluster_config=cluster_config, + namespace="test-namespace", + ) + + rayjob_cr = rayjob._build_rayjob_cr() + + cluster_config.build_ray_cluster_spec.assert_called_once_with( + cluster_name="test-job-cluster" + ) + assert "rayClusterSpec" in rayjob_cr["spec"] + assert rayjob_cr["spec"]["rayClusterSpec"] == mock_spec + + +def test_rayjob_with_runtime_env(auto_mock_setup): + """ + Test RayJob with runtime environment configuration. + """ + runtime_env = RuntimeEnv(pip=["numpy", "pandas"]) + + rayjob = RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + cluster_name="test-cluster", + runtime_env=runtime_env, + namespace="test-namespace", + ) + + assert rayjob.runtime_env == runtime_env + + rayjob_cr = rayjob._build_rayjob_cr() + assert rayjob_cr["spec"]["runtimeEnvYAML"] == "pip:\n- numpy\n- pandas\n" + + +def test_rayjob_with_runtime_env_dict(auto_mock_setup): + """ + Test RayJob with runtime environment as dict (user convenience). + Users can pass a dict instead of having to import RuntimeEnv. + """ + # User can pass dict instead of RuntimeEnv object + runtime_env_dict = { + "pip": ["numpy", "pandas"], + "env_vars": {"TEST_VAR": "test_value"}, + } + + rayjob = RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + cluster_name="test-cluster", + runtime_env=runtime_env_dict, + namespace="test-namespace", + ) + + # Should be converted to RuntimeEnv internally + assert isinstance(rayjob.runtime_env, RuntimeEnv) + assert rayjob.runtime_env["env_vars"] == {"TEST_VAR": "test_value"} + + # Verify it generates proper YAML output + rayjob_cr = rayjob._build_rayjob_cr() + assert "runtimeEnvYAML" in rayjob_cr["spec"] + runtime_yaml = rayjob_cr["spec"]["runtimeEnvYAML"] + assert "pip:" in runtime_yaml or "pip_packages:" in runtime_yaml + assert "env_vars:" in runtime_yaml + assert "TEST_VAR" in runtime_yaml + + +def test_rayjob_with_active_deadline_and_ttl(auto_mock_setup): + """ + Test RayJob with both active deadline and TTL settings. + """ + + rayjob = RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + cluster_name="test-cluster", + active_deadline_seconds=300, + ttl_seconds_after_finished=600, + namespace="test-namespace", + ) + + assert rayjob.active_deadline_seconds == 300 + assert rayjob.ttl_seconds_after_finished == 600 + + rayjob_cr = rayjob._build_rayjob_cr() + assert rayjob_cr["spec"]["activeDeadlineSeconds"] == 300 + assert rayjob_cr["spec"]["ttlSecondsAfterFinished"] == 600 + + +def test_rayjob_cluster_name_generation_with_config(auto_mock_setup): + """ + Test cluster name generation when using cluster_config. + """ + + cluster_config = ManagedClusterConfig() + + rayjob = RayJob( + job_name="my-job", + entrypoint="python -c 'print()'", + cluster_config=cluster_config, + namespace="test-namespace", # Explicitly specify namespace + ) + + assert rayjob.cluster_name == "my-job-cluster" + + +def test_rayjob_namespace_propagation_to_cluster_config(auto_mock_setup): + """ + Test that job namespace is propagated to cluster config when None. + """ + auto_mock_setup["get_current_namespace"].return_value = "detected-ns" + + cluster_config = ManagedClusterConfig() + + rayjob = RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + cluster_config=cluster_config, + ) + + assert rayjob.namespace == "detected-ns" + + +def test_rayjob_error_handling_invalid_cluster_config(auto_mock_setup): + """ + Test error handling with invalid cluster configuration. + """ + + with pytest.raises(ValueError): + RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + ) + + +def test_rayjob_constructor_parameter_validation(auto_mock_setup): + """ + Test constructor parameter validation. + """ + rayjob = RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + cluster_name="test-cluster", + namespace="test-ns", + runtime_env=RuntimeEnv(pip=["numpy"]), + ttl_seconds_after_finished=300, + active_deadline_seconds=600, + ) + + assert rayjob.name == "test-job" + assert rayjob.entrypoint == "python -c 'print()'" + assert rayjob.cluster_name == "test-cluster" + assert rayjob.namespace == "test-ns" + # Check that runtime_env is a RuntimeEnv object and contains pip dependencies + assert isinstance(rayjob.runtime_env, RuntimeEnv) + runtime_env_dict = rayjob.runtime_env.to_dict() + assert "pip" in runtime_env_dict + # Ray transforms pip to dict format with 'packages' key + assert runtime_env_dict["pip"]["packages"] == ["numpy"] + assert rayjob.ttl_seconds_after_finished == 300 + assert rayjob.active_deadline_seconds == 600 + + +def test_build_ray_cluster_spec_function(): + """ + Test the build_ray_cluster_spec method directly. + """ + cluster_config = ManagedClusterConfig( + num_workers=2, + head_cpu_requests="500m", + head_memory_requests="512Mi", + worker_cpu_requests="250m", + worker_memory_requests="256Mi", + ) + + spec = cluster_config.build_ray_cluster_spec("test-cluster") + assert "rayVersion" in spec + assert "enableInTreeAutoscaling" in spec + assert spec["enableInTreeAutoscaling"] is False # Required for Kueue + assert "headGroupSpec" in spec + assert "workerGroupSpecs" in spec + + head_spec = spec["headGroupSpec"] + assert head_spec["serviceType"] == "ClusterIP" + assert head_spec["enableIngress"] is False + assert "rayStartParams" in head_spec + assert "template" in head_spec + worker_specs = spec["workerGroupSpecs"] + assert len(worker_specs) == 1 + worker_spec = worker_specs[0] + assert worker_spec["replicas"] == 2 + assert worker_spec["minReplicas"] == 2 + assert worker_spec["maxReplicas"] == 2 + assert worker_spec["groupName"] == "worker-group-test-cluster" + + +def test_build_ray_cluster_spec_with_accelerators(): + """ + Test build_ray_cluster_spec with GPU accelerators. + """ + cluster_config = ManagedClusterConfig( + head_accelerators={"nvidia.com/gpu": 1}, + worker_accelerators={"nvidia.com/gpu": 2}, + ) + + spec = cluster_config.build_ray_cluster_spec("test-cluster") + head_spec = spec["headGroupSpec"] + head_params = head_spec["rayStartParams"] + assert "num-gpus" in head_params + assert head_params["num-gpus"] == "1" + + worker_specs = spec["workerGroupSpecs"] + worker_spec = worker_specs[0] + worker_params = worker_spec["rayStartParams"] + assert "num-gpus" in worker_params + assert worker_params["num-gpus"] == "2" + + +def test_build_ray_cluster_spec_with_custom_volumes(): + """ + Test build_ray_cluster_spec with custom volumes and volume mounts. + """ + custom_volume = V1Volume(name="custom-data", empty_dir={}) + custom_volume_mount = V1VolumeMount(name="custom-data", mount_path="/data") + cluster_config = ManagedClusterConfig( + volumes=[custom_volume], + volume_mounts=[custom_volume_mount], + ) + + spec = cluster_config.build_ray_cluster_spec("test-cluster") + head_spec = spec["headGroupSpec"] + head_pod_spec = head_spec["template"].spec + assert len(head_pod_spec.volumes) > 0 + + head_container = head_pod_spec.containers[0] + assert len(head_container.volume_mounts) > 0 + + +def test_build_ray_cluster_spec_with_environment_variables(): + """ + Test build_ray_cluster_spec with environment variables. + """ + cluster_config = ManagedClusterConfig( + envs={"CUDA_VISIBLE_DEVICES": "0", "RAY_DISABLE_IMPORT_WARNING": "1"}, + ) + + spec = cluster_config.build_ray_cluster_spec("test-cluster") + + head_spec = spec["headGroupSpec"] + head_pod_spec = head_spec["template"].spec + head_container = head_pod_spec.containers[0] + assert hasattr(head_container, "env") + env_vars = {env.name: env.value for env in head_container.env} + assert env_vars["CUDA_VISIBLE_DEVICES"] == "0" + assert env_vars["RAY_DISABLE_IMPORT_WARNING"] == "1" + worker_specs = spec["workerGroupSpecs"] + worker_spec = worker_specs[0] + worker_pod_spec = worker_spec["template"].spec + worker_container = worker_pod_spec.containers[0] + + assert hasattr(worker_container, "env") + worker_env_vars = {env.name: env.value for env in worker_container.env} + assert worker_env_vars["CUDA_VISIBLE_DEVICES"] == "0" + assert worker_env_vars["RAY_DISABLE_IMPORT_WARNING"] == "1" + + +def test_build_ray_cluster_spec_with_tolerations(): + """ + Test build_ray_cluster_spec with tolerations. + """ + head_toleration = V1Toleration( + key="node-role.kubernetes.io/master", operator="Exists", effect="NoSchedule" + ) + worker_toleration = V1Toleration( + key="nvidia.com/gpu", operator="Exists", effect="NoSchedule" + ) + + cluster_config = ManagedClusterConfig( + head_tolerations=[head_toleration], + worker_tolerations=[worker_toleration], + ) + + spec = cluster_config.build_ray_cluster_spec("test-cluster") + head_spec = spec["headGroupSpec"] + head_pod_spec = head_spec["template"].spec + assert hasattr(head_pod_spec, "tolerations") + assert len(head_pod_spec.tolerations) == 1 + assert head_pod_spec.tolerations[0].key == "node-role.kubernetes.io/master" + + worker_specs = spec["workerGroupSpecs"] + worker_spec = worker_specs[0] + worker_pod_spec = worker_spec["template"].spec + assert hasattr(worker_pod_spec, "tolerations") + assert len(worker_pod_spec.tolerations) == 1 + assert worker_pod_spec.tolerations[0].key == "nvidia.com/gpu" + + +def test_build_ray_cluster_spec_with_image_pull_secrets(): + """ + Test build_ray_cluster_spec with image pull secrets. + """ + cluster_config = ManagedClusterConfig( + image_pull_secrets=["my-registry-secret", "another-secret"] + ) + + spec = cluster_config.build_ray_cluster_spec("test-cluster") + + head_spec = spec["headGroupSpec"] + head_pod_spec = head_spec["template"].spec + assert hasattr(head_pod_spec, "image_pull_secrets") + + head_secrets = head_pod_spec.image_pull_secrets + assert len(head_secrets) == 2 + assert head_secrets[0].name == "my-registry-secret" + assert head_secrets[1].name == "another-secret" + + worker_specs = spec["workerGroupSpecs"] + worker_spec = worker_specs[0] + worker_pod_spec = worker_spec["template"].spec + assert hasattr(worker_pod_spec, "image_pull_secrets") + + worker_secrets = worker_pod_spec.image_pull_secrets + assert len(worker_secrets) == 2 + assert worker_secrets[0].name == "my-registry-secret" + assert worker_secrets[1].name == "another-secret" + + +def test_submit_with_cluster_config_compatible_image_passes(auto_mock_setup): + """ + Test that submission passes with compatible cluster_config image. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + mock_api_instance.submit_job.return_value = True + + cluster_config = ManagedClusterConfig(image=f"ray:{RAY_VERSION}") + + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + namespace="test-namespace", + entrypoint="python -c 'print()'", + ) + + result = rayjob.submit() + assert result == "test-job" + + +def test_submit_with_cluster_config_incompatible_image_fails(auto_mock_setup): + """ + Test that submission fails with incompatible cluster_config image. + """ + + cluster_config = ManagedClusterConfig(image="ray:2.8.0") # Different version + + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + namespace="test-namespace", + entrypoint="python -c 'print()'", + ) + + with pytest.raises( + ValueError, match="Cluster config image: Ray version mismatch detected" + ): + rayjob.submit() + + +def test_validate_ray_version_compatibility_method(auto_mock_setup): + """ + Test the _validate_ray_version_compatibility method directly. + """ + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + namespace="test-namespace", + entrypoint="python -c 'print()'", + ) + + rayjob._validate_ray_version_compatibility() + rayjob._cluster_config = ManagedClusterConfig(image=f"ray:{RAY_VERSION}") + rayjob._validate_ray_version_compatibility() + rayjob._cluster_config = ManagedClusterConfig(image="ray:2.8.0") + with pytest.raises( + ValueError, match="Cluster config image: Ray version mismatch detected" + ): + rayjob._validate_ray_version_compatibility() + + rayjob._cluster_config = ManagedClusterConfig(image="custom-image:latest") + with pytest.warns( + UserWarning, match="Cluster config image: Cannot determine Ray version" + ): + rayjob._validate_ray_version_compatibility() + + +def test_validate_cluster_config_image_method(auto_mock_setup): + """ + Test the _validate_cluster_config_image method directly. + """ + + rayjob = RayJob( + job_name="test-job", + cluster_config=ManagedClusterConfig(), + namespace="test-namespace", + entrypoint="python -c 'print()'", + ) + + rayjob._validate_cluster_config_image() + rayjob._cluster_config.image = f"ray:{RAY_VERSION}" + rayjob._validate_cluster_config_image() + rayjob._cluster_config.image = "ray:2.8.0" + with pytest.raises( + ValueError, match="Cluster config image: Ray version mismatch detected" + ): + rayjob._validate_cluster_config_image() + + rayjob._cluster_config.image = "custom-image:latest" + with pytest.warns( + UserWarning, match="Cluster config image: Cannot determine Ray version" + ): + rayjob._validate_cluster_config_image() + + +def test_validate_cluster_config_image_edge_cases(auto_mock_setup): + """ + Test edge cases in _validate_cluster_config_image method. + """ + + rayjob = RayJob( + job_name="test-job", + cluster_config=ManagedClusterConfig(), + namespace="test-namespace", + entrypoint="python -c 'print()'", + ) + + rayjob._cluster_config.image = None + rayjob._validate_cluster_config_image() + rayjob._cluster_config.image = "" + rayjob._validate_cluster_config_image() + rayjob._cluster_config.image = 123 + rayjob._validate_cluster_config_image() + + class MockClusterConfig: + pass + + rayjob._cluster_config = MockClusterConfig() + rayjob._validate_cluster_config_image() + + +def test_rayjob_stop_success(auto_mock_setup, caplog): + """ + Test successful RayJob stop operation. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + + mock_api_instance.suspend_job.return_value = { + "metadata": {"name": "test-rayjob"}, + "spec": {"suspend": True}, + } + + rayjob = RayJob( + job_name="test-rayjob", + cluster_name="test-cluster", + namespace="test-namespace", + entrypoint="python -c 'print()'", + ) + + with caplog.at_level("INFO"): + result = rayjob.stop() + + assert result is True + + mock_api_instance.suspend_job.assert_called_once_with( + name="test-rayjob", k8s_namespace="test-namespace" + ) + + # Verify success message was logged + assert "Successfully stopped the RayJob test-rayjob" in caplog.text + + +def test_rayjob_stop_failure(auto_mock_setup): + """ + Test RayJob stop operation when API call fails. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + + mock_api_instance.suspend_job.return_value = None + + rayjob = RayJob( + job_name="test-rayjob", + cluster_name="test-cluster", + namespace="test-namespace", + entrypoint="python -c 'print()'", + ) + + with pytest.raises(RuntimeError, match="Failed to stop the RayJob test-rayjob"): + rayjob.stop() + + mock_api_instance.suspend_job.assert_called_once_with( + name="test-rayjob", k8s_namespace="test-namespace" + ) + + +def test_rayjob_resubmit_success(auto_mock_setup): + """ + Test successful RayJob resubmit operation. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + + mock_api_instance.resubmit_job.return_value = { + "metadata": {"name": "test-rayjob"}, + "spec": {"suspend": False}, + } + + rayjob = RayJob( + job_name="test-rayjob", + cluster_name="test-cluster", + namespace="test-namespace", + entrypoint="python -c 'print()'", + ) + + result = rayjob.resubmit() + + assert result is True + + mock_api_instance.resubmit_job.assert_called_once_with( + name="test-rayjob", k8s_namespace="test-namespace" + ) + + +def test_rayjob_resubmit_failure(auto_mock_setup): + """ + Test RayJob resubmit operation when API call fails. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + + mock_api_instance.resubmit_job.return_value = None + + rayjob = RayJob( + job_name="test-rayjob", + cluster_name="test-cluster", + namespace="test-namespace", + entrypoint="python -c 'print()'", + ) + + with pytest.raises(RuntimeError, match="Failed to resubmit the RayJob test-rayjob"): + rayjob.resubmit() + + mock_api_instance.resubmit_job.assert_called_once_with( + name="test-rayjob", k8s_namespace="test-namespace" + ) + + +def test_rayjob_delete_success(auto_mock_setup): + """ + Test successful RayJob deletion. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + + rayjob = RayJob( + job_name="test-rayjob", + entrypoint="python -c 'print()'", + cluster_name="test-cluster", + ) + + mock_api_instance.delete_job.return_value = True + + result = rayjob.delete() + + assert result is True + mock_api_instance.delete_job.assert_called_once_with( + name="test-rayjob", k8s_namespace="test-namespace" + ) + + +def test_rayjob_delete_already_deleted(auto_mock_setup, caplog): + """ + Test RayJob deletion when already deleted (should succeed gracefully). + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + + rayjob = RayJob( + job_name="test-rayjob", + entrypoint="python -c 'print()'", + cluster_name="test-cluster", + ) + + # Python client returns False when job doesn't exist/already deleted + mock_api_instance.delete_job.return_value = False + + with caplog.at_level("INFO"): + result = rayjob.delete() + + # Should succeed (not raise error) when already deleted + assert result is True + assert "already deleted or does not exist" in caplog.text + + mock_api_instance.delete_job.assert_called_once_with( + name="test-rayjob", k8s_namespace="test-namespace" + ) + + +def test_rayjob_init_both_none_error(auto_mock_setup): + """ + Test RayJob initialization error when both cluster_name and cluster_config are None. + """ + with pytest.raises( + ValueError, + match="Configuration Error: You must provide either 'cluster_name' .* or 'cluster_config'", + ): + RayJob( + job_name="test-job", + entrypoint="python -c 'print()'", + cluster_name=None, + cluster_config=None, + ) + + +def test_rayjob_init_missing_cluster_name_with_no_config(auto_mock_setup): + """ + Test RayJob initialization error when cluster_name is None without cluster_config. + """ + with pytest.raises( + ValueError, + match="Configuration Error: a 'cluster_name' is required when not providing 'cluster_config'", + ): + rayjob = RayJob.__new__(RayJob) + rayjob.name = "test-job" + rayjob.entrypoint = "python test.py" + rayjob.runtime_env = None + rayjob.ttl_seconds_after_finished = 0 + rayjob.active_deadline_seconds = None + rayjob.shutdown_after_job_finishes = True + rayjob.namespace = "test-namespace" + rayjob._cluster_name = None + rayjob._cluster_config = None + if rayjob._cluster_config is None and rayjob._cluster_name is None: + raise ValueError( + "❌ Configuration Error: a 'cluster_name' is required when not providing 'cluster_config'" + ) + + +def test_rayjob_kueue_label_no_default_queue(auto_mock_setup, mocker, caplog): + """ + Test RayJob falls back to 'default' queue when no default queue exists. + """ + mocker.patch( + "codeflare_sdk.ray.rayjobs.rayjob.get_default_kueue_name", + return_value=None, + ) + + mock_api_instance = auto_mock_setup["rayjob_api"] + mock_api_instance.submit_job.return_value = {"metadata": {"name": "test-job"}} + + cluster_config = ManagedClusterConfig() + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python -c 'print()'", + ) + + with caplog.at_level("WARNING"): + rayjob.submit() + + # Verify the submitted job has the fallback label + call_args = mock_api_instance.submit_job.call_args + submitted_job = call_args.kwargs["job"] + assert submitted_job["metadata"]["labels"]["kueue.x-k8s.io/queue-name"] == "default" + + # Verify warning was logged + assert "No default Kueue LocalQueue found" in caplog.text + + +def test_rayjob_kueue_explicit_local_queue(auto_mock_setup): + """ + Test RayJob uses explicitly specified local queue. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + mock_api_instance.submit_job.return_value = {"metadata": {"name": "test-job"}} + + cluster_config = ManagedClusterConfig() + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python -c 'print()'", + local_queue="custom-queue", + ) + + rayjob.submit() + + # Verify the submitted job has the explicit queue label + call_args = mock_api_instance.submit_job.call_args + submitted_job = call_args.kwargs["job"] + assert ( + submitted_job["metadata"]["labels"]["kueue.x-k8s.io/queue-name"] + == "custom-queue" + ) + + +def test_rayjob_no_kueue_label_for_existing_cluster(auto_mock_setup): + """ + Test RayJob doesn't add Kueue label for existing clusters. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + mock_api_instance.submit_job.return_value = {"metadata": {"name": "test-job"}} + + # Using existing cluster (no cluster_config) + rayjob = RayJob( + job_name="test-job", + cluster_name="existing-cluster", + entrypoint="python -c 'print()'", + ) + + rayjob.submit() + + # Verify no Kueue label was added + call_args = mock_api_instance.submit_job.call_args + submitted_job = call_args.kwargs["job"] + assert "kueue.x-k8s.io/queue-name" not in submitted_job["metadata"]["labels"] + + +def test_rayjob_with_ttl_and_deadline(auto_mock_setup): + """ + Test RayJob with TTL and active deadline seconds. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + mock_api_instance.submit_job.return_value = {"metadata": {"name": "test-job"}} + + cluster_config = ManagedClusterConfig() + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python -c 'print()'", + ttl_seconds_after_finished=300, + active_deadline_seconds=600, + ) + + rayjob.submit() + + # Verify TTL and deadline were set + call_args = mock_api_instance.submit_job.call_args + submitted_job = call_args.kwargs["job"] + assert submitted_job["spec"]["ttlSecondsAfterFinished"] == 300 + assert submitted_job["spec"]["activeDeadlineSeconds"] == 600 + + +def test_rayjob_shutdown_after_job_finishes(auto_mock_setup): + """ + Test RayJob sets shutdownAfterJobFinishes correctly. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + mock_api_instance.submit_job.return_value = {"metadata": {"name": "test-job"}} + + # Test with managed cluster (should shutdown) + cluster_config = ManagedClusterConfig() + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python -c 'print()'", + ) + + rayjob.submit() + + call_args = mock_api_instance.submit_job.call_args + submitted_job = call_args.kwargs["job"] + assert submitted_job["spec"]["shutdownAfterJobFinishes"] is True + + # Test with existing cluster (should not shutdown) + rayjob2 = RayJob( + job_name="test-job2", + cluster_name="existing-cluster", + entrypoint="python -c 'print()'", + ) + + rayjob2.submit() + + call_args2 = mock_api_instance.submit_job.call_args + submitted_job2 = call_args2.kwargs["job"] + assert submitted_job2["spec"]["shutdownAfterJobFinishes"] is False + + +def test_rayjob_stop_delete_resubmit_logging(auto_mock_setup, caplog): + """ + Test logging for stop, delete, and resubmit operations. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + + # Test stop with logging + mock_api_instance.suspend_job.return_value = { + "metadata": {"name": "test-rayjob"}, + "spec": {"suspend": True}, + } + + rayjob = RayJob( + job_name="test-rayjob", + cluster_name="test-cluster", + namespace="test-namespace", + entrypoint="python -c 'print()'", + ) + + with caplog.at_level("INFO"): + result = rayjob.stop() + + assert result is True + assert "Successfully stopped the RayJob test-rayjob" in caplog.text + + # Test delete with logging + caplog.clear() + mock_api_instance.delete_job.return_value = True + + with caplog.at_level("INFO"): + result = rayjob.delete() + + assert result is True + assert "Successfully deleted the RayJob test-rayjob" in caplog.text + + # Test resubmit with logging + caplog.clear() + mock_api_instance.resubmit_job.return_value = { + "metadata": {"name": "test-rayjob"}, + "spec": {"suspend": False}, + } + + with caplog.at_level("INFO"): + result = rayjob.resubmit() + + assert result is True + assert "Successfully resubmitted the RayJob test-rayjob" in caplog.text + + +def test_rayjob_initialization_logging(auto_mock_setup, caplog): + """ + Test RayJob initialization logging. + """ + with caplog.at_level("INFO"): + cluster_config = ManagedClusterConfig() + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python -c 'print()'", + ) + + assert "Creating new cluster: test-job-cluster" in caplog.text + assert "Initialized RayJob: test-job in namespace: test-namespace" in caplog.text + + +def test_build_submitter_pod_template_uses_default_image(auto_mock_setup, mocker): + """ + Test that _build_submitter_pod_template() uses get_ray_image_for_python_version() for default image. + """ + # Mock get_ray_image_for_python_version to verify it's called + mock_get_image = mocker.patch( + "codeflare_sdk.ray.rayjobs.rayjob.get_ray_image_for_python_version", + return_value="auto-detected-image:py3.12", + ) + + rayjob = RayJob( + job_name="test-job", + cluster_name="existing-cluster", + entrypoint="python -c 'print()'", + namespace="test-namespace", + ) + + files = {"test.py": "print('hello')"} + secret_name = "test-files" + + # Call _build_submitter_pod_template + submitter_template = rayjob._build_submitter_pod_template(files, secret_name) + + # Verify get_ray_image_for_python_version was called + mock_get_image.assert_called_once() + + # Verify the submitter pod uses the auto-detected image + assert ( + submitter_template["spec"]["containers"][0]["image"] + == "auto-detected-image:py3.12" + ) + + +def test_build_submitter_pod_template_uses_cluster_config_image( + auto_mock_setup, mocker +): + """ + Test that _build_submitter_pod_template() uses cluster_config image when provided. + """ + # Mock get_ray_image_for_python_version (should be called but overridden) + mock_get_image = mocker.patch( + "codeflare_sdk.ray.rayjobs.rayjob.get_ray_image_for_python_version", + return_value="auto-detected-image:py3.12", + ) + + cluster_config = ManagedClusterConfig(image="custom-cluster-image:v1") + + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python -c 'print()'", + namespace="test-namespace", + ) + + files = {"test.py": "print('hello')"} + secret_name = "test-files" + + # Call _build_submitter_pod_template + submitter_template = rayjob._build_submitter_pod_template(files, secret_name) + + # Verify get_ray_image_for_python_version was called + mock_get_image.assert_called_once() + + # Verify the submitter pod uses the cluster config image (overrides default) + assert ( + submitter_template["spec"]["containers"][0]["image"] + == "custom-cluster-image:v1" + ) + + +def test_build_submitter_pod_template_with_files(auto_mock_setup): + """ + Test that _build_submitter_pod_template() correctly builds Secret items for files. + """ + rayjob = RayJob( + job_name="test-job", + cluster_name="existing-cluster", + entrypoint="python -c 'print()'", + namespace="test-namespace", + ) + + files = {"main.py": "print('main')", "helper.py": "print('helper')"} + secret_name = "test-files" + + # Call _build_submitter_pod_template + submitter_template = rayjob._build_submitter_pod_template(files, secret_name) + + # Verify Secret items are created for each file + secret_items = submitter_template["spec"]["volumes"][0]["secret"]["items"] + assert len(secret_items) == 2 + + # Verify each file has a Secret item + file_names = [item["key"] for item in secret_items] + assert "main.py" in file_names + assert "helper.py" in file_names + + # Verify paths match keys + for item in secret_items: + assert item["key"] == item["path"] + + +def test_validate_working_dir_entrypoint_no_runtime_env(auto_mock_setup, tmp_path): + """ + Test validation checks file exists even when no runtime_env is specified. + """ + # Create the script file + script_file = tmp_path / "script.py" + script_file.write_text("print('hello')") + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint=f"python {script_file}", + namespace="test-namespace", + ) + + # Should not raise exception (file exists) + rayjob._validate_working_dir_entrypoint() + + +def test_validate_working_dir_entrypoint_no_working_dir(auto_mock_setup, tmp_path): + """ + Test validation checks file when runtime_env has no working_dir. + """ + # Create the script file + script_file = tmp_path / "script.py" + script_file.write_text("print('hello')") + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint=f"python {script_file}", + namespace="test-namespace", + runtime_env=RuntimeEnv(pip=["numpy"]), + ) + + # Should not raise exception (file exists) + rayjob._validate_working_dir_entrypoint() + + +def test_validate_working_dir_entrypoint_remote_working_dir(auto_mock_setup): + """ + Test validation skips ALL checks for remote working_dir. + """ + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint="python nonexistent_script.py", # File doesn't exist, but should be ignored + namespace="test-namespace", + runtime_env=RuntimeEnv( + working_dir="https://github.com/user/repo/archive/main.zip" + ), + ) + + # Should not raise any exception (remote working_dir skips all validation) + rayjob._validate_working_dir_entrypoint() + + +def test_validate_working_dir_entrypoint_no_python_file(auto_mock_setup): + """ + Test validation passes when entrypoint has no Python file reference. + """ + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint="echo 'hello world'", # No Python file + namespace="test-namespace", + runtime_env=RuntimeEnv(working_dir="."), + ) + + # Should not raise any exception + rayjob._validate_working_dir_entrypoint() + + +def test_validate_working_dir_entrypoint_no_redundancy(auto_mock_setup, tmp_path): + """ + Test validation passes when entrypoint doesn't reference working_dir. + """ + # Create test directory and file + test_dir = tmp_path / "testdir" + test_dir.mkdir() + script_file = test_dir / "script.py" + script_file.write_text("print('hello')") + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint="python script.py", # No directory prefix + namespace="test-namespace", + runtime_env=RuntimeEnv(working_dir=str(test_dir)), + ) + + # Should not raise any exception + rayjob._validate_working_dir_entrypoint() + + +def test_validate_working_dir_entrypoint_redundant_reference_error( + auto_mock_setup, tmp_path +): + """ + Test validation raises error when entrypoint redundantly references working_dir. + """ + # Create test directory and file + test_dir = tmp_path / "testdir" + test_dir.mkdir() + script_file = test_dir / "script.py" + script_file.write_text("print('hello')") + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint=f"python {test_dir}/script.py", # Redundant reference + namespace="test-namespace", + runtime_env=RuntimeEnv(working_dir=str(test_dir)), + ) + + # Should raise ValueError with helpful message + with pytest.raises(ValueError, match="Working directory conflict detected"): + rayjob._validate_working_dir_entrypoint() + + +def test_validate_working_dir_entrypoint_with_dot_slash(auto_mock_setup, tmp_path): + """ + Test validation handles paths with ./ prefix correctly. + """ + # Create test directory and file + test_dir = tmp_path / "testdir" + test_dir.mkdir() + script_file = test_dir / "script.py" + script_file.write_text("print('hello')") + + # Change to temp directory so relative paths work + import os + + original_cwd = os.getcwd() + try: + os.chdir(tmp_path) + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint="python ./testdir/script.py", # With ./ prefix + namespace="test-namespace", + runtime_env=RuntimeEnv(working_dir="./testdir"), # With ./ prefix + ) + + # Should raise ValueError (redundant reference) + with pytest.raises(ValueError, match="Working directory conflict detected"): + rayjob._validate_working_dir_entrypoint() + finally: + os.chdir(original_cwd) + + +def test_validate_working_dir_entrypoint_submit_integration(auto_mock_setup, tmp_path): + """ + Test that validation is called during submit() and blocks submission. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + mock_api_instance.submit_job.return_value = {"metadata": {"name": "test-job"}} + + # Create test directory and file + test_dir = tmp_path / "testdir" + test_dir.mkdir() + script_file = test_dir / "script.py" + script_file.write_text("print('hello')") + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint=f"python {test_dir}/script.py", # Redundant reference + namespace="test-namespace", + runtime_env=RuntimeEnv(working_dir=str(test_dir)), + ) + + # Should raise ValueError during submit() before API call + with pytest.raises(ValueError, match="Working directory conflict detected"): + rayjob.submit() + + # Verify submit_job was never called (validation blocked it) + mock_api_instance.submit_job.assert_not_called() + + +def test_validate_working_dir_entrypoint_error_message_format( + auto_mock_setup, tmp_path +): + """ + Test that error message contains helpful information. + """ + # Create test directory and file + test_dir = tmp_path / "testdir" + test_dir.mkdir() + script_file = test_dir / "script.py" + script_file.write_text("print('hello')") + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint=f"python {test_dir}/script.py", + namespace="test-namespace", + runtime_env=RuntimeEnv(working_dir=str(test_dir)), + ) + + try: + rayjob._validate_working_dir_entrypoint() + assert False, "Should have raised ValueError" + except ValueError as e: + error_msg = str(e) + # Verify error message contains key information + assert "Working directory conflict detected" in error_msg + assert "working_dir:" in error_msg + assert "entrypoint references:" in error_msg + assert "Fix: Remove the directory prefix" in error_msg + assert "python script.py" in error_msg # Suggested fix + + +def test_validate_working_dir_entrypoint_subdirectory_valid(auto_mock_setup, tmp_path): + """ + Test validation passes when entrypoint references subdirectory within working_dir. + """ + # Create test directory structure: testdir/subdir/script.py + test_dir = tmp_path / "testdir" + test_dir.mkdir() + sub_dir = test_dir / "subdir" + sub_dir.mkdir() + script_file = sub_dir / "script.py" + script_file.write_text("print('hello')") + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint="python subdir/script.py", # Correct: relative to working_dir + namespace="test-namespace", + runtime_env=RuntimeEnv(working_dir=str(test_dir)), + ) + + # Should not raise any exception + rayjob._validate_working_dir_entrypoint() + + +def test_validate_working_dir_entrypoint_runtime_env_as_dict(auto_mock_setup, tmp_path): + """ + Test validation works when runtime_env is passed as dict (not RuntimeEnv object). + """ + # Create test directory and file + test_dir = tmp_path / "testdir" + test_dir.mkdir() + script_file = test_dir / "script.py" + script_file.write_text("print('hello')") + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint=f"python {test_dir}/script.py", # Redundant reference + namespace="test-namespace", + runtime_env={"working_dir": str(test_dir)}, # Dict instead of RuntimeEnv + ) + + # Should raise ValueError even with dict runtime_env + with pytest.raises(ValueError, match="Working directory conflict detected"): + rayjob._validate_working_dir_entrypoint() + + +def test_validate_file_exists_with_working_dir(auto_mock_setup, tmp_path): + """ + Test validation checks that entrypoint file exists within working_dir. + """ + # Create working directory but NOT the script file + test_dir = tmp_path / "testdir" + test_dir.mkdir() + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint="python script.py", # File doesn't exist + namespace="test-namespace", + runtime_env=RuntimeEnv(working_dir=str(test_dir)), + ) + + # Should raise ValueError about missing file + with pytest.raises(ValueError, match="Entrypoint file not found"): + rayjob._validate_working_dir_entrypoint() + + +def test_validate_file_exists_without_working_dir(auto_mock_setup, tmp_path): + """ + Test validation checks that entrypoint file exists when no working_dir and using ./ prefix. + """ + # Don't create the script file + script_path = "./missing_script.py" + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint=f"python {script_path}", # File doesn't exist (local path with ./) + namespace="test-namespace", + ) + + # Should raise ValueError about missing file + with pytest.raises(ValueError, match="Entrypoint file not found"): + rayjob._validate_working_dir_entrypoint() + + +def test_validate_existing_file_with_working_dir_passes(auto_mock_setup, tmp_path): + """ + Test validation passes when file exists in working_dir. + """ + # Create working directory AND the script file + test_dir = tmp_path / "testdir" + test_dir.mkdir() + script_file = test_dir / "script.py" + script_file.write_text("print('hello')") + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint="python script.py", # File exists + namespace="test-namespace", + runtime_env=RuntimeEnv(working_dir=str(test_dir)), + ) + + # Should not raise any exception + rayjob._validate_working_dir_entrypoint() + + +def test_validate_inline_python_command_skipped(auto_mock_setup): + """ + Test validation skips inline Python commands (no file reference). + """ + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint="python -c 'print(\"hello world\")'", # No file reference + namespace="test-namespace", + ) + + # Should not raise any exception (no file to validate) + rayjob._validate_working_dir_entrypoint() + + +def test_validate_simple_filename_without_working_dir_missing(auto_mock_setup): + """ + Test validation checks simple filenames without working_dir. + """ + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint="python script.py", # File doesn't exist locally + namespace="test-namespace", + ) + + # Should raise ValueError (file will be extracted from local, so must exist) + with pytest.raises(ValueError, match="Entrypoint file not found"): + rayjob._validate_working_dir_entrypoint() + + +def test_validate_simple_filename_without_working_dir_exists(auto_mock_setup, tmp_path): + """ + Test validation passes when simple filename exists locally without working_dir. + """ + import os + + original_cwd = os.getcwd() + try: + os.chdir(tmp_path) + # Create the script file in current directory + script_file = tmp_path / "script.py" + script_file.write_text("print('hello')") + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + entrypoint="python script.py", # Simple filename exists locally + namespace="test-namespace", + ) + + # Should not raise exception (file exists) + rayjob._validate_working_dir_entrypoint() + finally: + os.chdir(original_cwd) diff --git a/src/codeflare_sdk/ray/rayjobs/test/test_runtime_env.py b/src/codeflare_sdk/ray/rayjobs/test/test_runtime_env.py new file mode 100644 index 00000000..e059a8d3 --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/test/test_runtime_env.py @@ -0,0 +1,1061 @@ +# Copyright 2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import pytest +import os +import io +from unittest.mock import MagicMock, patch +from codeflare_sdk.common.utils.constants import MOUNT_PATH, RAY_VERSION +from ray.runtime_env import RuntimeEnv + +from codeflare_sdk.ray.rayjobs.rayjob import RayJob +from codeflare_sdk.ray.cluster.config import ClusterConfiguration +from codeflare_sdk.ray.rayjobs.config import ManagedClusterConfig +from kubernetes.client import ( + V1Volume, + V1VolumeMount, + ApiException, +) + +from codeflare_sdk.ray.rayjobs.runtime_env import ( + create_secret_from_spec, + extract_all_local_files, +) + + +def test_rayjob_with_remote_working_dir(auto_mock_setup): + """ + Test RayJob with remote working directory in runtime_env. + Should not extract local files and should pass through remote URL. + """ + runtime_env = RuntimeEnv( + working_dir="https://github.com/org/repo/archive/refs/heads/main.zip", + pip=["numpy", "pandas"], + env_vars={"TEST_VAR": "test_value"}, + ) + + rayjob = RayJob( + job_name="test-job", + entrypoint="python test.py", + cluster_name="test-cluster", + runtime_env=runtime_env, + namespace="test-namespace", + ) + + assert rayjob.runtime_env == runtime_env + + # Should not extract any local files due to remote working_dir + files = extract_all_local_files(rayjob) + assert files is None + + rayjob_cr = rayjob._build_rayjob_cr() + + # Should have runtimeEnvYAML with all fields + expected_runtime_env = ( + "env_vars:\n" + " TEST_VAR: test_value\n" + "pip:\n" + "- numpy\n" + "- pandas\n" + "working_dir: https://github.com/org/repo/archive/refs/heads/main.zip\n" + ) + assert rayjob_cr["spec"]["runtimeEnvYAML"] == expected_runtime_env + + # Should not have submitterPodTemplate since no local files + assert "submitterPodTemplate" not in rayjob_cr["spec"] + + # Entrypoint should be unchanged + assert rayjob_cr["spec"]["entrypoint"] == "python test.py" + + +def test_build_file_secret_spec(): + """ + Test building Secret specification for files. + """ + config = ManagedClusterConfig() + files = {"main.py": "print('main')", "helper.py": "print('helper')"} + + spec = config.build_file_secret_spec( + job_name="test-job", namespace="test-namespace", files=files + ) + + assert spec["apiVersion"] == "v1" + assert spec["kind"] == "Secret" + assert spec["type"] == "Opaque" + assert spec["metadata"]["name"] == "test-job-files" + assert spec["metadata"]["namespace"] == "test-namespace" + assert spec["data"] == files + + +def test_build_file_volume_specs(): + """ + Test building volume and mount specifications for files. + """ + config = ManagedClusterConfig() + + volume_spec, mount_spec = config.build_file_volume_specs( + secret_name="test-files", mount_path="/custom/path" + ) + + assert volume_spec["name"] == "ray-job-files" + assert volume_spec["secret"]["secretName"] == "test-files" + + assert mount_spec["name"] == "ray-job-files" + assert mount_spec["mountPath"] == "/custom/path" + + +def test_add_file_volumes(): + """ + Test adding file volumes to cluster configuration. + """ + config = ManagedClusterConfig() + + # Initially no volumes + assert len(config.volumes) == 0 + assert len(config.volume_mounts) == 0 + + config.add_file_volumes(secret_name="test-files") + + assert len(config.volumes) == 1 + assert len(config.volume_mounts) == 1 + + volume = config.volumes[0] + mount = config.volume_mounts[0] + + assert volume.name == "ray-job-files" + assert volume.secret.secret_name == "test-files" + + assert mount.name == "ray-job-files" + assert mount.mount_path == MOUNT_PATH + + +def test_add_file_volumes_duplicate_prevention(): + """ + Test that adding file volumes twice doesn't create duplicates. + """ + config = ManagedClusterConfig() + + # Add volumes twice + config.add_file_volumes(secret_name="test-files") + config.add_file_volumes(secret_name="test-files") + + assert len(config.volumes) == 1 + assert len(config.volume_mounts) == 1 + + +def test_create_secret_from_spec(auto_mock_setup): + """ + Test creating Secret via Kubernetes API. + """ + mock_api_instance = auto_mock_setup["k8s_api"] + + rayjob = RayJob( + job_name="test-job", + cluster_name="existing-cluster", + entrypoint="python test.py", + namespace="test-namespace", + ) + + secret_spec = { + "apiVersion": "v1", + "kind": "Secret", + "type": "Opaque", + "metadata": {"name": "test-files", "namespace": "test-namespace"}, + "data": {"test.py": "print('test')"}, + } + + # Provide valid RayJob result with UID as KubeRay client would + rayjob_result = { + "metadata": { + "name": "test-job", + "namespace": "test-namespace", + "uid": "test-uid-12345", + } + } + + result = create_secret_from_spec(rayjob, secret_spec, rayjob_result) + + assert result == "test-files" + mock_api_instance.create_namespaced_secret.assert_called_once() + + +def test_create_secret_already_exists(auto_mock_setup): + """ + Test creating Secret when it already exists (409 conflict). + """ + mock_api_instance = auto_mock_setup["k8s_api"] + + mock_api_instance.create_namespaced_secret.side_effect = ApiException(status=409) + + rayjob = RayJob( + job_name="test-job", + cluster_name="existing-cluster", + entrypoint="python test.py", + namespace="test-namespace", + ) + + secret_spec = { + "apiVersion": "v1", + "kind": "Secret", + "type": "Opaque", + "metadata": {"name": "test-files", "namespace": "test-namespace"}, + "data": {"test.py": "print('test')"}, + } + + # Provide valid RayJob result with UID as KubeRay client would + rayjob_result = { + "metadata": { + "name": "test-job", + "namespace": "test-namespace", + "uid": "test-uid-67890", + } + } + + result = create_secret_from_spec(rayjob, secret_spec, rayjob_result) + + assert result == "test-files" + mock_api_instance.create_namespaced_secret.assert_called_once() + mock_api_instance.replace_namespaced_secret.assert_called_once() + + +def test_create_secret_with_owner_reference_basic(mocker, auto_mock_setup, caplog): + """ + Test creating Secret with owner reference from valid RayJob result. + """ + mock_api_instance = auto_mock_setup["k8s_api"] + + # Mock client.V1ObjectMeta and V1Secret + mock_v1_metadata = mocker.patch("kubernetes.client.V1ObjectMeta") + mock_metadata_instance = MagicMock() + mock_v1_metadata.return_value = mock_metadata_instance + + rayjob = RayJob( + job_name="test-job", + cluster_name="existing-cluster", + entrypoint="python test.py", + namespace="test-namespace", + ) + + secret_spec = { + "apiVersion": "v1", + "kind": "Secret", + "type": "Opaque", + "metadata": { + "name": "test-files", + "namespace": "test-namespace", + "labels": { + "ray.io/job-name": "test-job", + "app.kubernetes.io/managed-by": "codeflare-sdk", + "app.kubernetes.io/component": "rayjob-files", + }, + }, + "data": {"test.py": "print('test')"}, + } + + # Valid RayJob result with UID + rayjob_result = { + "metadata": { + "name": "test-job", + "namespace": "test-namespace", + "uid": "a4dd4c5a-ab61-411d-b4d1-4abb5177422a", + } + } + + with caplog.at_level("INFO"): + result = create_secret_from_spec(rayjob, secret_spec, rayjob_result) + + assert result == "test-files" + + # Verify owner reference was set + expected_owner_ref = mocker.ANY # We'll check via the logs + assert ( + "Adding owner reference to Secret 'test-files' with RayJob UID: a4dd4c5a-ab61-411d-b4d1-4abb5177422a" + in caplog.text + ) + + assert mock_metadata_instance.owner_references is not None + mock_api_instance.create_namespaced_secret.assert_called_once() + + +def test_file_handling_kubernetes_best_practice_flow(mocker, tmp_path): + """ + Test the Kubernetes best practice flow: pre-declare volume, submit, create Secret. + """ + mocker.patch("kubernetes.config.load_kube_config") + + mock_api_class = mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayjobApi") + mock_api_instance = MagicMock() + mock_api_class.return_value = mock_api_instance + + submit_result = { + "metadata": { + "name": "test-job", + "namespace": "test-namespace", + "uid": "test-uid-12345", + } + } + mock_api_instance.submit_job.return_value = submit_result + + # Mock create_file_secret where it's used (imported into rayjob module) + mock_create_secret = mocker.patch( + "codeflare_sdk.ray.rayjobs.rayjob.create_file_secret" + ) + mock_add_volumes = mocker.patch.object(ManagedClusterConfig, "add_file_volumes") + + # RayClusterApi is already mocked by auto_mock_setup + + test_file = tmp_path / "test.py" + test_file.write_text("print('test')") + + call_order = [] + + def track_add_volumes(*args, **kwargs): + call_order.append("add_volumes") + # Should be called with Secret name + assert args[0] == "test-job-files" + + def track_submit(*args, **kwargs): + call_order.append("submit_job") + return submit_result + + def track_create_secret(*args, **kwargs): + call_order.append("create_secret") + # Args should be: (job, files, rayjob_result) + assert len(args) >= 3, f"Expected 3 args, got {len(args)}: {args}" + assert args[2] == submit_result # rayjob_result should be third arg + + mock_add_volumes.side_effect = track_add_volumes + mock_api_instance.submit_job.side_effect = track_submit + mock_create_secret.side_effect = track_create_secret + + original_cwd = os.getcwd() + try: + os.chdir(tmp_path) + + cluster_config = ManagedClusterConfig() + + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python test.py", + namespace="test-namespace", + ) + + rayjob.submit() + finally: + os.chdir(original_cwd) + + # Verify the order: submit → create Secret + assert call_order == ["submit_job", "create_secret"] + + mock_api_instance.submit_job.assert_called_once() + mock_create_secret.assert_called_once() + + # Verify create_file_secret was called with: (job, files, rayjob_result) + # Files dict includes metadata key __entrypoint_path__ for single file case + call_args = mock_create_secret.call_args[0] + assert call_args[0] == rayjob + assert call_args[2] == submit_result + # Check that the actual file content is present + assert "test.py" in call_args[1] + assert call_args[1]["test.py"] == "print('test')" + + +def test_rayjob_submit_with_files_new_cluster(auto_mock_setup, tmp_path): + """ + Test RayJob submission with file detection for new cluster. + """ + mock_api_instance = auto_mock_setup["rayjob_api"] + mock_api_instance.submit_job.return_value = { + "metadata": { + "name": "test-job", + "namespace": "test-namespace", + "uid": "test-uid-files-12345", + } + } + + mock_k8s_instance = auto_mock_setup["k8s_api"] + + # Create test file + test_file = tmp_path / "test.py" + test_file.write_text("print('Hello from the test file!')") + + cluster_config = ManagedClusterConfig() + + original_cwd = os.getcwd() + os.chdir(tmp_path) + + try: + rayjob = RayJob( + job_name="test-job", + cluster_config=cluster_config, + entrypoint="python test.py", + namespace="test-namespace", + ) + + # Submit should detect files and handle them + result = rayjob.submit() + + assert result == "test-job" + + mock_k8s_instance.create_namespaced_secret.assert_called_once() + + assert len(cluster_config.volumes) == 0 + assert len(cluster_config.volume_mounts) == 0 + # Entrypoint should be adjusted to use just the filename + assert rayjob.entrypoint == "python test.py" + + finally: + os.chdir(original_cwd) + + +def test_create_secret_api_error_non_409(auto_mock_setup): + """ + Test create_secret_from_spec handles non-409 API errors. + """ + mock_api_instance = auto_mock_setup["k8s_api"] + + # Configure to raise 500 error + mock_api_instance.create_namespaced_secret.side_effect = ApiException(status=500) + + rayjob = RayJob( + job_name="test-job", + cluster_name="existing-cluster", + entrypoint="python test.py", + namespace="test-namespace", + ) + + secret_spec = { + "apiVersion": "v1", + "kind": "Secret", + "type": "Opaque", + "metadata": {"name": "test-files", "namespace": "test-namespace"}, + "data": {"test.py": "print('test')"}, + } + + # Provide valid RayJob result with UID as KubeRay client would + rayjob_result = { + "metadata": { + "name": "test-job", + "namespace": "test-namespace", + "uid": "test-uid-api-error", + } + } + + with pytest.raises(RuntimeError, match="Failed to create Secret"): + create_secret_from_spec(rayjob, secret_spec, rayjob_result) + + +def test_add_file_volumes_existing_volume_skip(): + """ + Test add_file_volumes skips when volume already exists (missing coverage). + """ + from kubernetes.client import V1SecretVolumeSource + + config = ManagedClusterConfig() + + # Pre-add a volume with same name + existing_volume = V1Volume( + name="ray-job-files", + secret=V1SecretVolumeSource(secret_name="existing-files"), + ) + config.volumes.append(existing_volume) + + config.add_file_volumes(secret_name="new-files") + assert len(config.volumes) == 1 + assert len(config.volume_mounts) == 0 # Mount not added due to volume skip + + +def test_add_file_volumes_existing_mount_skip(): + """ + Test add_file_volumes skips when mount already exists (missing coverage). + """ + config = ManagedClusterConfig() + + # Pre-add a mount with same name + existing_mount = V1VolumeMount(name="ray-job-files", mount_path="/existing/path") + config.volume_mounts.append(existing_mount) + + config.add_file_volumes(secret_name="new-files") + assert len(config.volumes) == 0 # Volume not added due to mount skip + assert len(config.volume_mounts) == 1 + + +def test_zip_directory_functionality(tmp_path): + """ + Test _zip_directory with real directories and files. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import _zip_directory + + # Create test directory structure + test_dir = tmp_path / "working_dir" + test_dir.mkdir() + + # Create some test files + (test_dir / "main.py").write_text("print('main script')") + (test_dir / "utils.py").write_text("def helper(): pass") + + # Create subdirectory with file + sub_dir = test_dir / "subdir" + sub_dir.mkdir() + (sub_dir / "nested.py").write_text("print('nested file')") + + # Test zipping + zip_data = _zip_directory(str(test_dir)) + + assert zip_data is not None + assert len(zip_data) > 0 + assert isinstance(zip_data, bytes) + + +def test_zip_directory_excludes_jupyter_notebooks(tmp_path, caplog): + """ + Test that Jupyter notebook files (.ipynb) and markdown files (.md) are excluded from zip. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import _zip_directory + import zipfile + + # Create test directory with mixed file types + test_dir = tmp_path / "working_dir" + test_dir.mkdir() + + # Create Python files (should be included) + (test_dir / "main.py").write_text("print('main script')") + (test_dir / "utils.py").write_text("def helper(): pass") + + # Create Jupyter notebook files (should be excluded) + (test_dir / "analysis.ipynb").write_text('{"cells": [], "metadata": {}}') + (test_dir / "experiment.IPYNB").write_text( + '{"cells": [], "metadata": {}}' + ) # Test case insensitive + + # Create markdown files (should be excluded) + (test_dir / "README.md").write_text("# Project Documentation\n") + (test_dir / "CHANGELOG.MD").write_text("# Changes\n") # Test case insensitive + + # Create subdirectory with mixed files + sub_dir = test_dir / "notebooks" + sub_dir.mkdir() + (sub_dir / "data_exploration.ipynb").write_text('{"cells": [], "metadata": {}}') + (sub_dir / "helper.py").write_text("print('nested file')") + (sub_dir / "guide.md").write_text("# Guide\n") + + # Test zipping + with caplog.at_level("INFO"): + zip_data = _zip_directory(str(test_dir)) + + assert zip_data is not None + assert len(zip_data) > 0 + + # Verify log message includes exclusion count (3 ipynb + 3 md = 6 total) + assert "Excluded 6 file(s) (.ipynb, .md)" in caplog.text + + # Verify excluded files are not in the zip + zip_buffer = io.BytesIO(zip_data) + with zipfile.ZipFile(zip_buffer, "r") as zipf: + zip_contents = zipf.namelist() + + # Python files should be present + assert "main.py" in zip_contents + assert "utils.py" in zip_contents + assert "notebooks/helper.py" in zip_contents + + # Jupyter notebooks should be excluded + assert "analysis.ipynb" not in zip_contents + assert "experiment.IPYNB" not in zip_contents + assert "notebooks/data_exploration.ipynb" not in zip_contents + + # Markdown files should be excluded + assert "README.md" not in zip_contents + assert "CHANGELOG.MD" not in zip_contents + assert "notebooks/guide.md" not in zip_contents + + +def test_zip_directory_no_exclusions_when_no_notebooks(tmp_path, caplog): + """ + Test that no exclusion message is logged when no notebook or markdown files exist. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import _zip_directory + + # Create test directory with only Python files + test_dir = tmp_path / "working_dir" + test_dir.mkdir() + (test_dir / "main.py").write_text("print('main script')") + (test_dir / "utils.py").write_text("def helper(): pass") + + # Test zipping + with caplog.at_level("INFO"): + zip_data = _zip_directory(str(test_dir)) + + assert zip_data is not None + + # Verify log message does NOT mention exclusions + assert "Excluded" not in caplog.text + + +def test_should_exclude_file_function(): + """ + Test the _should_exclude_file helper function directly. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import _should_exclude_file + + # Should exclude .ipynb files (case insensitive) + assert _should_exclude_file("notebook.ipynb") is True + assert _should_exclude_file("analysis.IPYNB") is True + assert _should_exclude_file("data/exploration.ipynb") is True + assert _should_exclude_file("subdir/nested.Ipynb") is True + + # Should exclude .md files (case insensitive) + assert _should_exclude_file("README.md") is True + assert _should_exclude_file("CHANGELOG.MD") is True + assert _should_exclude_file("docs/guide.md") is True + assert _should_exclude_file("subdir/notes.Md") is True + + # Should NOT exclude other files + assert _should_exclude_file("script.py") is False + assert _should_exclude_file("data.json") is False + assert _should_exclude_file("requirements.txt") is False + assert _should_exclude_file("model.pkl") is False + assert _should_exclude_file("markdown_parser.py") is False # Not .md + assert _should_exclude_file("test.html") is False + + +def test_zip_directory_error_handling(): + """ + Test _zip_directory error handling for IO errors during zipping. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import _zip_directory + + # Mock os.walk to raise an OSError + with patch("os.walk", side_effect=OSError("Permission denied")): + zip_data = _zip_directory("/some/path") + assert zip_data is None + + +def test_extract_all_local_files_with_working_dir(tmp_path): + """ + Test extract_all_local_files with local working directory. + """ + # Create test working directory + working_dir = tmp_path / "working_dir" + working_dir.mkdir() + (working_dir / "script.py").write_text("print('working dir script')") + + runtime_env = RuntimeEnv(working_dir=str(working_dir)) + + rayjob = RayJob( + job_name="test-job", + entrypoint="python script.py", + runtime_env=runtime_env, + namespace="test-namespace", + cluster_name="test-cluster", + ) + + files = extract_all_local_files(rayjob) + + assert files is not None + assert "working_dir.zip" in files + assert isinstance(files["working_dir.zip"], str) # base64 encoded + + # Verify it's valid base64 + import base64 + + try: + decoded = base64.b64decode(files["working_dir.zip"]) + assert len(decoded) > 0 + except Exception: + pytest.fail("Invalid base64 encoding") + + +def test_extract_all_local_files_excludes_notebooks(tmp_path, caplog): + """ + Test that extract_all_local_files excludes Jupyter notebooks and markdown files when zipping working directory. + """ + import zipfile + import base64 + + # Create test working directory with mixed files + working_dir = tmp_path / "working_dir" + working_dir.mkdir() + + # Python files that should be included + (working_dir / "main.py").write_text("print('main script')") + (working_dir / "helper.py").write_text("def helper_function(): pass") + + # Jupyter notebooks that should be excluded + (working_dir / "analysis.ipynb").write_text( + '{"cells": [{"cell_type": "code", "source": ["print(\'hello\')"]}]}' + ) + (working_dir / "data.ipynb").write_text('{"cells": [], "metadata": {}}') + + # Markdown files that should be excluded + (working_dir / "README.md").write_text("# Project Documentation\n") + (working_dir / "CHANGELOG.md").write_text("# Changes\n") + + runtime_env = RuntimeEnv(working_dir=str(working_dir)) + + rayjob = RayJob( + job_name="test-job", + entrypoint="python main.py", + runtime_env=runtime_env, + namespace="test-namespace", + cluster_name="test-cluster", + ) + + # This should zip the directory and exclude notebooks and markdown files + with caplog.at_level("INFO"): + files = extract_all_local_files(rayjob) + + assert files is not None + assert "working_dir.zip" in files + + # Verify exclusion was logged (2 ipynb + 2 md = 4 total) + assert "Excluded 4 file(s) (.ipynb, .md)" in caplog.text + + # Decode and verify zip contents + zip_data = base64.b64decode(files["working_dir.zip"]) + zip_buffer = io.BytesIO(zip_data) + + with zipfile.ZipFile(zip_buffer, "r") as zipf: + zip_contents = zipf.namelist() + + # Python files should be present + assert "main.py" in zip_contents + assert "helper.py" in zip_contents + + # Jupyter notebooks should be excluded + assert "analysis.ipynb" not in zip_contents + assert "data.ipynb" not in zip_contents + + # Markdown files should be excluded + assert "README.md" not in zip_contents + assert "CHANGELOG.md" not in zip_contents + + +def test_extract_single_entrypoint_file_error_handling(tmp_path): + """ + Test _extract_single_entrypoint_file with file read errors. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import _extract_single_entrypoint_file + + # Create a file that exists but make it unreadable + test_file = tmp_path / "unreadable.py" + test_file.write_text("print('test')") + + rayjob = RayJob( + job_name="test-job", + entrypoint=f"python {test_file}", + namespace="test-namespace", + cluster_name="test-cluster", + ) + + # Mock open to raise IOError + with patch("builtins.open", side_effect=IOError("Permission denied")): + result = _extract_single_entrypoint_file(rayjob) + assert result is None + + +def test_extract_single_entrypoint_file_no_match(): + """ + Test _extract_single_entrypoint_file with no Python file matches. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import _extract_single_entrypoint_file + + rayjob = RayJob( + job_name="test-job", + entrypoint="echo 'no python files here'", + namespace="test-namespace", + cluster_name="test-cluster", + ) + + result = _extract_single_entrypoint_file(rayjob) + assert result is None + + +def test_parse_requirements_file_valid(tmp_path): + """ + Test parse_requirements_file with valid requirements.txt. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import parse_requirements_file + + # Create test requirements file + req_file = tmp_path / "requirements.txt" + req_file.write_text( + """# This is a comment +numpy==1.21.0 +pandas>=1.3.0 + +# Another comment +scikit-learn +""" + ) + + result = parse_requirements_file(str(req_file)) + + assert result is not None + assert len(result) == 3 + assert "numpy==1.21.0" in result + assert "pandas>=1.3.0" in result + assert "scikit-learn" in result + # Comments and empty lines should be filtered out + assert "# This is a comment" not in result + + +def test_parse_requirements_file_missing(): + """ + Test parse_requirements_file with non-existent file. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import parse_requirements_file + + result = parse_requirements_file("/non/existent/requirements.txt") + assert result is None + + +def test_parse_requirements_file_read_error(tmp_path): + """ + Test parse_requirements_file with file read error. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import parse_requirements_file + + # Create a file + req_file = tmp_path / "requirements.txt" + req_file.write_text("numpy==1.21.0") + + # Mock open to raise IOError + with patch("builtins.open", side_effect=IOError("Permission denied")): + result = parse_requirements_file(str(req_file)) + assert result is None + + +def test_process_pip_dependencies_list(): + """ + Test process_pip_dependencies with list input. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import process_pip_dependencies + + rayjob = RayJob( + job_name="test-job", + entrypoint="python test.py", + namespace="test-namespace", + cluster_name="test-cluster", + ) + + pip_list = ["numpy", "pandas", "scikit-learn"] + result = process_pip_dependencies(rayjob, pip_list) + + assert result == pip_list + + +def test_process_pip_dependencies_requirements_file(tmp_path): + """ + Test process_pip_dependencies with requirements.txt path. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import process_pip_dependencies + + # Create test requirements file + req_file = tmp_path / "requirements.txt" + req_file.write_text("numpy==1.21.0\npandas>=1.3.0") + + rayjob = RayJob( + job_name="test-job", + entrypoint="python test.py", + namespace="test-namespace", + cluster_name="test-cluster", + ) + + result = process_pip_dependencies(rayjob, str(req_file)) + + assert result is not None + assert len(result) == 2 + assert "numpy==1.21.0" in result + assert "pandas>=1.3.0" in result + + +def test_process_pip_dependencies_dict_format(): + """ + Test process_pip_dependencies with dict format containing packages. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import process_pip_dependencies + + rayjob = RayJob( + job_name="test-job", + entrypoint="python test.py", + namespace="test-namespace", + cluster_name="test-cluster", + ) + + pip_dict = {"packages": ["numpy", "pandas"], "pip_check": False} + result = process_pip_dependencies(rayjob, pip_dict) + + assert result == ["numpy", "pandas"] + + +def test_process_pip_dependencies_unsupported_format(): + """ + Test process_pip_dependencies with unsupported format. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import process_pip_dependencies + + rayjob = RayJob( + job_name="test-job", + entrypoint="python test.py", + namespace="test-namespace", + cluster_name="test-cluster", + ) + + # Test with unsupported format (int) + result = process_pip_dependencies(rayjob, 12345) + assert result is None + + +def test_process_runtime_env_local_working_dir(tmp_path): + """ + Test process_runtime_env with local working directory. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import process_runtime_env, UNZIP_PATH + + # Create test working directory + working_dir = tmp_path / "working_dir" + working_dir.mkdir() + (working_dir / "script.py").write_text("print('test')") + + runtime_env = RuntimeEnv( + working_dir=str(working_dir), + env_vars={"TEST_VAR": "test_value"}, + ) + + rayjob = RayJob( + job_name="test-job", + entrypoint="python script.py", + runtime_env=runtime_env, + namespace="test-namespace", + cluster_name="test-cluster", + ) + + result = process_runtime_env(rayjob) + + assert result is not None + assert f"working_dir: {UNZIP_PATH}" in result + assert "env_vars:" in result + assert "TEST_VAR: test_value" in result + + +def test_process_runtime_env_single_file_case(tmp_path): + """ + Test process_runtime_env with single file case (no working_dir in runtime_env). + """ + from codeflare_sdk.ray.rayjobs.runtime_env import process_runtime_env + from codeflare_sdk.common.utils.constants import MOUNT_PATH + + rayjob = RayJob( + job_name="test-job", + entrypoint="python test.py", + namespace="test-namespace", + cluster_name="test-cluster", + ) + + # Files dict without working_dir.zip (single file case) + files = {"test.py": "print('test')"} + + result = process_runtime_env(rayjob, files) + + assert result is not None + assert f"working_dir: {MOUNT_PATH}" in result + + +def test_process_runtime_env_no_processing_needed(): + """ + Test process_runtime_env returns None when no processing needed. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import process_runtime_env + + rayjob = RayJob( + job_name="test-job", + entrypoint="python test.py", + namespace="test-namespace", + cluster_name="test-cluster", + ) + + # No runtime_env and no files + result = process_runtime_env(rayjob) + assert result is None + + +def test_normalize_runtime_env_with_none(): + """ + Test _normalize_runtime_env with None input. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import _normalize_runtime_env + + result = _normalize_runtime_env(None) + assert result is None + + +def test_extract_single_entrypoint_file_no_entrypoint(): + """ + Test _extract_single_entrypoint_file with no entrypoint. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import _extract_single_entrypoint_file + + rayjob = RayJob( + job_name="test-job", + entrypoint=None, # No entrypoint + namespace="test-namespace", + cluster_name="test-cluster", + ) + + result = _extract_single_entrypoint_file(rayjob) + assert result is None + + +def test_create_file_secret_filters_metadata_keys(auto_mock_setup, tmp_path): + """ + Test create_file_secret filters out metadata keys from files dict. + """ + from codeflare_sdk.ray.rayjobs.runtime_env import create_file_secret + + rayjob = RayJob( + job_name="test-job", + cluster_name="existing-cluster", + entrypoint="python test.py", + namespace="test-namespace", + ) + + # Files dict with metadata key that should be filtered out + files = { + "__entrypoint_path__": "some/path/test.py", # Should be filtered + "test.py": "print('test')", # Should remain + } + + rayjob_result = { + "metadata": { + "name": "test-job", + "namespace": "test-namespace", + "uid": "test-uid-12345", + } + } + + # This should not raise an error and should filter out metadata keys + create_file_secret(rayjob, files, rayjob_result) + + # Verify the Secret was created (mocked) + mock_api_instance = auto_mock_setup["k8s_api"] + mock_api_instance.create_namespaced_secret.assert_called_once() + + # The call should have filtered data (only test.py, not __entrypoint_path__) + call_args = mock_api_instance.create_namespaced_secret.call_args + secret_data = call_args[1]["body"].string_data # Changed from data to string_data + assert "test.py" in secret_data + assert "__entrypoint_path__" not in secret_data diff --git a/src/codeflare_sdk/ray/rayjobs/test/test_status.py b/src/codeflare_sdk/ray/rayjobs/test/test_status.py new file mode 100644 index 00000000..2f2b9957 --- /dev/null +++ b/src/codeflare_sdk/ray/rayjobs/test/test_status.py @@ -0,0 +1,373 @@ +# Copyright 2025 IBM, Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from codeflare_sdk.ray.rayjobs.rayjob import RayJob +from codeflare_sdk.ray.rayjobs.status import ( + CodeflareRayJobStatus, + RayJobDeploymentStatus, + RayJobInfo, +) + + +def test_rayjob_status(mocker): + """ + Test the RayJob status method with different deployment statuses. + """ + # Mock kubernetes config loading + mocker.patch("kubernetes.config.load_kube_config") + # Mock the RayjobApi to avoid actual Kubernetes calls + mock_api_class = mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayjobApi") + mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayClusterApi") + mock_api_instance = mock_api_class.return_value + + # Create a RayJob instance + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + namespace="test-ns", + entrypoint="python test.py", + ) + + # Test case 1: No job found + mock_api_instance.get_job_status.return_value = None + status, ready = rayjob.status(print_to_console=False) + assert status == CodeflareRayJobStatus.UNKNOWN + assert ready == False + + # Test case 2: Running job + mock_api_instance.get_job_status.return_value = { + "jobId": "test-job-abc123", + "jobDeploymentStatus": "Running", + "startTime": "2025-07-28T11:37:07Z", + "failed": 0, + "succeeded": 0, + "rayClusterName": "test-cluster", + } + status, ready = rayjob.status(print_to_console=False) + assert status == CodeflareRayJobStatus.RUNNING + assert ready == False + + # Test case 3: Complete job + mock_api_instance.get_job_status.return_value = { + "jobId": "test-job-abc123", + "jobDeploymentStatus": "Complete", + "startTime": "2025-07-28T11:37:07Z", + "endTime": "2025-07-28T11:42:30Z", + "failed": 0, + "succeeded": 1, + "rayClusterName": "test-cluster", + } + status, ready = rayjob.status(print_to_console=False) + assert status == CodeflareRayJobStatus.COMPLETE + assert ready == True + + # Test case 4: Failed job + mock_api_instance.get_job_status.return_value = { + "jobId": "test-job-abc123", + "jobDeploymentStatus": "Failed", + "startTime": "2025-07-28T11:37:07Z", + "endTime": "2025-07-28T11:42:30Z", + "failed": 1, + "succeeded": 0, + "rayClusterName": "test-cluster", + } + status, ready = rayjob.status(print_to_console=False) + assert status == CodeflareRayJobStatus.FAILED + assert ready == False + + # Test case 5: Suspended job + mock_api_instance.get_job_status.return_value = { + "jobId": "test-job-abc123", + "jobDeploymentStatus": "Suspended", + "startTime": "2025-07-28T11:37:07Z", + "failed": 0, + "succeeded": 0, + "rayClusterName": "test-cluster", + } + status, ready = rayjob.status(print_to_console=False) + assert status == CodeflareRayJobStatus.SUSPENDED + assert ready == False + + +def test_rayjob_status_unknown_deployment_status(mocker): + """ + Test handling of unknown deployment status from the API. + """ + mocker.patch("kubernetes.config.load_kube_config") + mock_api_class = mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayjobApi") + mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayClusterApi") + mock_api_instance = mock_api_class.return_value + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + namespace="test-ns", + entrypoint="python test.py", + ) + + # Test with unrecognized deployment status + mock_api_instance.get_job_status.return_value = { + "jobId": "test-job-abc123", + "jobDeploymentStatus": "SomeNewStatus", # Unknown status + "startTime": "2025-07-28T11:37:07Z", + "failed": 0, + "succeeded": 0, + } + + status, ready = rayjob.status(print_to_console=False) + assert status == CodeflareRayJobStatus.UNKNOWN + assert ready == False + + +def test_rayjob_status_missing_fields(mocker): + """ + Test handling of API response with missing fields. + """ + mocker.patch("kubernetes.config.load_kube_config") + mock_api_class = mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayjobApi") + mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayClusterApi") + mock_api_instance = mock_api_class.return_value + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + namespace="test-ns", + entrypoint="python test.py", + ) + + # Test with minimal API response (missing some fields) + mock_api_instance.get_job_status.return_value = { + # Missing jobId, failed, succeeded, etc. + "jobDeploymentStatus": "Running", + } + + status, ready = rayjob.status(print_to_console=False) + assert status == CodeflareRayJobStatus.RUNNING + assert ready == False + + +def test_map_to_codeflare_status(mocker): + """ + Test the _map_to_codeflare_status helper method directly. + """ + # Mock kubernetes config loading + mocker.patch("kubernetes.config.load_kube_config") + # Mock the RayjobApi constructor to avoid authentication issues + mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayjobApi") + mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayClusterApi") + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + namespace="test-ns", + entrypoint="python test.py", + ) + + # Test all deployment status mappings + status, ready = rayjob._map_to_codeflare_status(RayJobDeploymentStatus.COMPLETE) + assert status == CodeflareRayJobStatus.COMPLETE + assert ready == True + + status, ready = rayjob._map_to_codeflare_status(RayJobDeploymentStatus.RUNNING) + assert status == CodeflareRayJobStatus.RUNNING + assert ready == False + + status, ready = rayjob._map_to_codeflare_status(RayJobDeploymentStatus.FAILED) + assert status == CodeflareRayJobStatus.FAILED + assert ready == False + + status, ready = rayjob._map_to_codeflare_status(RayJobDeploymentStatus.SUSPENDED) + assert status == CodeflareRayJobStatus.SUSPENDED + assert ready == False + + status, ready = rayjob._map_to_codeflare_status(RayJobDeploymentStatus.UNKNOWN) + assert status == CodeflareRayJobStatus.UNKNOWN + assert ready == False + + +def test_rayjob_info_dataclass(): + """ + Test the RayJobInfo dataclass creation and field access. + """ + job_info = RayJobInfo( + name="test-job", + job_id="test-job-abc123", + status=RayJobDeploymentStatus.RUNNING, + namespace="test-ns", + cluster_name="test-cluster", + start_time="2025-07-28T11:37:07Z", + failed_attempts=0, + succeeded_attempts=0, + ) + + # Test all fields are accessible + assert job_info.name == "test-job" + assert job_info.job_id == "test-job-abc123" + assert job_info.status == RayJobDeploymentStatus.RUNNING + assert job_info.namespace == "test-ns" + assert job_info.cluster_name == "test-cluster" + assert job_info.start_time == "2025-07-28T11:37:07Z" + assert job_info.end_time is None # Default value + assert job_info.failed_attempts == 0 + assert job_info.succeeded_attempts == 0 + + +def test_rayjob_status_print_no_job_found(mocker): + """ + Test that pretty_print.print_no_job_found is called when no job is found and print_to_console=True. + """ + mocker.patch("kubernetes.config.load_kube_config") + # Mock the RayjobApi and pretty_print + mock_api_class = mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayjobApi") + mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayClusterApi") + mock_api_instance = mock_api_class.return_value + mock_print_no_job_found = mocker.patch( + "codeflare_sdk.ray.rayjobs.pretty_print.print_no_job_found" + ) + + # Create a RayJob instance + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + namespace="test-ns", + entrypoint="python test.py", + ) + + # No job found scenario + mock_api_instance.get_job_status.return_value = None + + # Call status with print_to_console=True + status, ready = rayjob.status(print_to_console=True) + + # Verify the pretty print function was called + mock_print_no_job_found.assert_called_once_with("test-job", "test-ns") + assert status == CodeflareRayJobStatus.UNKNOWN + assert ready == False + + +def test_rayjob_status_print_job_found(mocker): + """ + Test that pretty_print.print_job_status is called when job is found and print_to_console=True. + """ + mocker.patch("kubernetes.config.load_kube_config") + # Mock the RayjobApi and pretty_print + mock_api_class = mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayjobApi") + mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayClusterApi") + mock_api_instance = mock_api_class.return_value + mock_print_job_status = mocker.patch( + "codeflare_sdk.ray.rayjobs.pretty_print.print_job_status" + ) + + # Create a RayJob instance + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + namespace="test-ns", + entrypoint="python test.py", + ) + + # Job found scenario + mock_api_instance.get_job_status.return_value = { + "jobId": "test-job-abc123", + "jobDeploymentStatus": "Running", + "startTime": "2025-07-28T11:37:07Z", + "failed": 0, + "succeeded": 0, + "rayClusterName": "test-cluster", + } + + # Call status with print_to_console=True + status, ready = rayjob.status(print_to_console=True) + + # Verify the pretty print function was called + mock_print_job_status.assert_called_once() + # Verify the RayJobInfo object passed to print_job_status + call_args = mock_print_job_status.call_args[0][0] # First positional argument + assert call_args.name == "test-job" + assert call_args.job_id == "test-job-abc123" + assert call_args.status == RayJobDeploymentStatus.RUNNING + assert call_args.namespace == "test-ns" + assert call_args.cluster_name == "test-cluster" + + assert status == CodeflareRayJobStatus.RUNNING + assert ready == False + + +def test_rayjob_status_all_deployment_states(mocker): + """Test RayJob status method with all deployment states.""" + mocker.patch("kubernetes.config.load_kube_config") + mock_api_class = mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayjobApi") + mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayClusterApi") + mock_api_instance = mock_api_class.return_value + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + namespace="test-ns", + entrypoint="python test.py", + ) + + # Test mapping of deployment statuses to CodeflareRayJobStatus + test_cases = [ + # (deployment_status_str, expected CodeflareRayJobStatus, expected ready) + ("Complete", CodeflareRayJobStatus.COMPLETE, True), + ("Running", CodeflareRayJobStatus.RUNNING, False), + ("Failed", CodeflareRayJobStatus.FAILED, False), + ("Suspended", CodeflareRayJobStatus.SUSPENDED, False), + ] + + for deployment_status_str, expected_status, expected_ready in test_cases: + mock_api_instance.get_job_status.return_value = { + "jobId": "test-job-abc123", + "jobDeploymentStatus": deployment_status_str, + "startTime": "2025-07-28T11:37:07Z", + "failed": 0, + "succeeded": 0, + "rayClusterName": "test-cluster", + } + status, ready = rayjob.status(print_to_console=False) + assert status == expected_status, f"Failed for {deployment_status_str}" + assert ( + ready == expected_ready + ), f"Failed ready check for {deployment_status_str}" + + +def test_rayjob_status_with_end_time(mocker): + """Test RayJob status with end time field.""" + mocker.patch("kubernetes.config.load_kube_config") + mock_api_class = mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayjobApi") + mocker.patch("codeflare_sdk.ray.rayjobs.rayjob.RayClusterApi") + mock_api_instance = mock_api_class.return_value + + rayjob = RayJob( + job_name="test-job", + cluster_name="test-cluster", + namespace="test-ns", + entrypoint="python test.py", + ) + + # Test with end time field + mock_api_instance.get_job_status.return_value = { + "jobId": "test-job-abc123", + "jobDeploymentStatus": "Complete", + "startTime": "2025-07-28T11:37:07Z", + "endTime": "2025-07-28T11:47:07Z", + "failed": 0, + "succeeded": 1, + "rayClusterName": "test-cluster", + } + + status, ready = rayjob.status(print_to_console=False) + assert status == CodeflareRayJobStatus.COMPLETE + assert ready == True diff --git a/src/codeflare_sdk/vendored/.gitignore b/src/codeflare_sdk/vendored/.gitignore new file mode 100644 index 00000000..d6d73f9c --- /dev/null +++ b/src/codeflare_sdk/vendored/.gitignore @@ -0,0 +1,35 @@ + + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + + +# Distribution / packaging +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +.tox/ +htmlcov +.coverage +.cache +nosetests.xml +coverage.xml diff --git a/src/codeflare_sdk/vendored/LICENSE b/src/codeflare_sdk/vendored/LICENSE new file mode 100644 index 00000000..1dcfa84a --- /dev/null +++ b/src/codeflare_sdk/vendored/LICENSE @@ -0,0 +1,272 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------------------------------------- + +Code in python/ray/rllib/{evolution_strategies, dqn} adapted from +https://github.com/openai (MIT License) + +Copyright (c) 2016 OpenAI (http://openai.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +-------------------------------------------------------------------------------- + +Code in python/ray/rllib/impala/vtrace.py from +https://github.com/deepmind/scalable_agent + +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- +Code in python/ray/rllib/ars is adapted from https://github.com/modestyachts/ARS + +Copyright (c) 2018, ARS contributors (Horia Mania, Aurelia Guy, Benjamin Recht) +All rights reserved. + +Redistribution and use of ARS in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/codeflare_sdk/vendored/README.md b/src/codeflare_sdk/vendored/README.md new file mode 100644 index 00000000..2189cb5b --- /dev/null +++ b/src/codeflare_sdk/vendored/README.md @@ -0,0 +1,158 @@ +# Overview + +This python client library provide APIs to handle `raycluster` and `rayjobs` from your python application. + +## Prerequisites + +It is assumed that your `k8s cluster in already setup`. Your kubectl configuration is expected to be +in `~/.kube/config` if you are running the code directly from you terminal. + +It is also expected that the `kuberay operator` is installed. +[Installation instructions are here][quick-start] + +## Usage + +There are multiple levels of using the API with increasing levels of complexity. + +### director + +This is the easiest form of using the API to create rayclusters with predefined cluster sizes + +```python +my_kuberay_api = kuberay_cluster_api.RayClusterApi() + +my_cluster_director = kuberay_cluster_builder.Director() + +cluster0 = my_cluster_director.build_small_cluster(name="new-cluster0") + +if cluster0: + my_kuberay_api.create_ray_cluster(body=cluster0) +``` + +the director create the cluster definition, and the `cluster_api` acts as the HTTP client sending +the create (post) request to the k8s api-server + +### cluster_builder + +The builder allows you to build the cluster piece by piece. You can customize the cluster more. + +```python +cluster1 = ( + my_cluster_builder.build_meta(name="new-cluster1") + .build_head() + .build_worker(group_name="workers", replicas=3) + .get_cluster() + ) + +if not my_cluster_builder.succeeded: + return + +my_kuberay_api.create_ray_cluster(body=cluster1) +``` + +### cluster_utils + +`cluster_utils` gives you even more options to modify your cluster definition, add/remove worker +groups, change replicas in a worker group, duplicate a worker group, etc. + +```python +my_Cluster_utils = kuberay_cluster_utils.ClusterUtils() + +cluster_to_patch, succeeded = my_Cluster_utils.update_worker_group_replicas( + cluster2, group_name="workers", max_replicas=4, min_replicas=1, replicas=2 +) + +if succeeded: + my_kuberay_api.patch_ray_cluster( + name=cluster_to_patch["metadata"]["name"], ray_patch=cluster_to_patch + ) +``` + +### cluster_api + +The `cluster_api` is the one you always use to implement your cluster change in k8s. You can +use it with raw `JSON` if you wish. The `director/cluster_builder/cluster_utils` are just tools to +shield the user from using raw `JSON`. + +### job_api + +Finally, the `job_api` can be used to submit RayJobs to a pre-existing RayCluster. + +#### Submitting to Existing Cluster + +```python +from codeflare_sdk.vendored.python_client import kuberay_job_api, kuberay_cluster_api, constants + +job_body = { + "apiVersion": "ray.io/v1", + "kind": "RayJob", + "metadata": {...}, + "spec": { + "clusterSelector": { + "ray.io/cluster": "ray-cluster-name", + }, + "entrypoint": 'python -c training_script.py', + "submissionMode": "K8sJobMode", + }, +} + +kuberay_job_api.submit_job( + job=job_body, + k8s_namespace=namespace, +) +``` + +## Code Organization + +```text +clients/ +└── python-client + ├── examples + │ ├── complete-example.py + │ ├── use-builder.py + │ ├── use-director.py + │ ├── use-raw-config_map_with-api.py + │ ├── use-raw-with-api.py + │ └── use-utils.py + ├── LICENSE + ├── poetry.lock + ├── pyproject.toml + ├── python_client + │ ├── __init__.py + │ ├── constants.py + │ ├── kuberay_cluster_api.py + │ ├── kuberay_job_api.py + │ └── utils + │ ├── __init__.py + │ ├── kuberay_cluster_builder.py + │ └── kuberay_cluster_utils.py + ├── python_client_test + │ ├── README.md + │ ├── test_cluster_api.py + │ ├── test_director.py + │ ├── test_job_api.py + │ └── test_utils.py + └── README.md +``` + +## For developers + +make sure you have installed setuptool + +`pip install -U pip setuptools` + +### run the pip command + +from the directory `path/to/kuberay/clients/python-client` + +`pip install -e .` + +### to uninstall the module run + +`pip uninstall python-client` + +### For testing run + + `python -m unittest discover 'path/to/kuberay/clients/python-client/python_client_test/'` + +[quick-start]: https://github.com/ray-project/kuberay#quick-start diff --git a/src/codeflare_sdk/vendored/__init__.py b/src/codeflare_sdk/vendored/__init__.py new file mode 100644 index 00000000..93f1b14f --- /dev/null +++ b/src/codeflare_sdk/vendored/__init__.py @@ -0,0 +1,14 @@ +""" +Vendored third-party dependencies. + +This directory contains code from external projects that are bundled +with codeflare-sdk to avoid PyPI publishing restrictions. + +Contents: +- python_client: KubeRay Python client from ray-project/kuberay + Source: https://github.com/ray-project/kuberay @ b2fd91b58c2bbe22f9b4f730c5a8f3180c05e570 + License: Apache 2.0 (see LICENSE file) + + Vendored because the python-client is not published to PyPI and PyPI + does not allow direct git dependencies. +""" diff --git a/src/codeflare_sdk/vendored/examples/complete-example.py b/src/codeflare_sdk/vendored/examples/complete-example.py new file mode 100644 index 00000000..8cfdfdcc --- /dev/null +++ b/src/codeflare_sdk/vendored/examples/complete-example.py @@ -0,0 +1,144 @@ +import sys +import os +from os import path + + +""" +in case you are working directly with the source, and don't wish to +install the module with pip install, you can directly import the packages by uncommenting the following code. +""" + +""" +sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) + +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) +sibling_dirs = [ + d for d in os.listdir(parent_dir) if os.path.isdir(os.path.join(parent_dir, d)) +] +for sibling_dir in sibling_dirs: + sys.path.append(os.path.join(parent_dir, sibling_dir)) +""" + +from codeflare_sdk.vendored.python_client import kuberay_cluster_api + +from codeflare_sdk.vendored.python_client.utils import ( + kuberay_cluster_utils, + kuberay_cluster_builder, +) + + +def main(): + print("starting cluster handler...") + my_kuberay_api = kuberay_cluster_api.RayClusterApi() # this is the main api object + + my_cluster_director = ( + kuberay_cluster_builder.Director() + ) # this is the director object, to create a cluster with a single line of code + + my_cluster_builder = ( + kuberay_cluster_builder.ClusterBuilder() + ) # this is the builder object, to create a cluster with a more granular control + + my_Cluster_utils = ( + kuberay_cluster_utils.ClusterUtils() + ) # this is the utils object, to perform operations on a cluster + + cluster0 = my_cluster_director.build_small_cluster( + name="new-cluster0", labels={"demo-cluster": "yes"} + ) # this is the cluster object, it is a dict + + if cluster0: + my_kuberay_api.create_ray_cluster( + body=cluster0 + ) # this is the api call to create the cluster0 in k8s + + cluster1 = ( + my_cluster_builder.build_meta( + name="new-cluster1", labels={"demo-cluster": "yes"} + ) + .build_head() + .build_worker(group_name="workers") + .get_cluster() + ) + + if not my_cluster_builder.succeeded: + print("error building the cluster, aborting...") + return + my_kuberay_api.create_ray_cluster( + body=cluster1 + ) # this is the api call to create the cluster1 in k8s + + cluster2 = ( + my_cluster_builder.build_meta( + name="new-cluster2", labels={"demo-cluster": "yes"} + ) + .build_head() + .build_worker(group_name="workers") + .get_cluster() + ) + + if not my_cluster_builder.succeeded: + print("error building the cluster, aborting...") + return + + my_kuberay_api.create_ray_cluster( + body=cluster2 + ) # this is the api call to create the cluster2 in k8s + + # modifying the number of replicas in the workergroup + cluster_to_patch, succeeded = my_Cluster_utils.update_worker_group_replicas( + cluster2, group_name="workers", max_replicas=4, min_replicas=1, replicas=2 + ) + + if succeeded: + print( + "trying to patch raycluster = {}".format( + cluster_to_patch["metadata"]["name"] + ) + ) + my_kuberay_api.patch_ray_cluster( + name=cluster_to_patch["metadata"]["name"], ray_patch=cluster_to_patch + ) # this is the api call to patch the cluster2 in k8s + + cluster_to_patch, succeeded = my_Cluster_utils.duplicate_worker_group( + cluster1, group_name="workers", new_group_name="new-workers" + ) # this is the call to duplicate the worker group in cluster1 + if succeeded: + print( + "trying to patch raycluster = {}".format( + cluster_to_patch["metadata"]["name"] + ) + ) + my_kuberay_api.patch_ray_cluster( + name=cluster_to_patch["metadata"]["name"], ray_patch=cluster_to_patch + ) # this is the api call to patch the cluster1 in k8s + + kube_ray_list = my_kuberay_api.list_ray_clusters( + k8s_namespace="default", label_selector="demo-cluster=yes" + ) # this is the api call to list all the clusters in k8s + if "items" in kube_ray_list: + line = "-" * 72 + print(line) + print("{:<63s}{:>2s}".format("Name", "Namespace")) + print(line) + for cluster in kube_ray_list["items"]: + print( + "{:<63s}{:>2s}".format( + cluster["metadata"]["name"], + cluster["metadata"]["namespace"], + ) + ) + print(line) + + if "items" in kube_ray_list: + for cluster in kube_ray_list["items"]: + print("deleting raycluster = {}".format(cluster["metadata"]["name"])) + my_kuberay_api.delete_ray_cluster( + name=cluster["metadata"]["name"], + k8s_namespace=cluster["metadata"]["namespace"], + ) # this is the api call to delete the cluster in k8s + + +if __name__ == "__main__": + main() diff --git a/src/codeflare_sdk/vendored/examples/use-builder.py b/src/codeflare_sdk/vendored/examples/use-builder.py new file mode 100644 index 00000000..5309fc00 --- /dev/null +++ b/src/codeflare_sdk/vendored/examples/use-builder.py @@ -0,0 +1,79 @@ +import sys +import os +from os import path +import json + + +""" +in case you are working directly with the source, and don't wish to +install the module with pip install, you can directly import the packages by uncommenting the following code. +""" + +""" +sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) + +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) +sibling_dirs = [ + d for d in os.listdir(parent_dir) if os.path.isdir(os.path.join(parent_dir, d)) +] +for sibling_dir in sibling_dirs: + sys.path.append(os.path.join(parent_dir, sibling_dir)) +""" + +from codeflare_sdk.vendored.python_client import kuberay_cluster_api + +from codeflare_sdk.vendored.python_client.utils import kuberay_cluster_builder + + +def main(): + print("starting cluster handler...") + my_kuberay_api = kuberay_cluster_api.RayClusterApi() + + my_cluster_builder = kuberay_cluster_builder.ClusterBuilder() + + cluster1 = ( + my_cluster_builder.build_meta( + name="new-cluster1", labels={"demo-cluster": "yes"} + ) + .build_head() + .build_worker(group_name="workers") + .get_cluster() + ) + + if not my_cluster_builder.succeeded: + print("error building the cluster, aborting...") + return + + print("creating raycluster = {}".format(cluster1["metadata"]["name"])) + my_kuberay_api.create_ray_cluster(body=cluster1) + + # the rest of the code is simply to list and cleanup the created cluster + kube_ray_list = my_kuberay_api.list_ray_clusters( + k8s_namespace="default", label_selector="demo-cluster=yes" + ) + if "items" in kube_ray_list: + line = "-" * 72 + print(line) + print("{:<63s}{:>2s}".format("Name", "Namespace")) + print(line) + for cluster in kube_ray_list["items"]: + print( + "{:<63s}{:>2s}".format( + cluster["metadata"]["name"], + cluster["metadata"]["namespace"], + ) + ) + print(line) + + if "items" in kube_ray_list: + for cluster in kube_ray_list["items"]: + print("deleting raycluster = {}".format(cluster["metadata"]["name"])) + my_kuberay_api.delete_ray_cluster( + name=cluster["metadata"]["name"], + k8s_namespace=cluster["metadata"]["namespace"], + ) + + +if __name__ == "__main__": + main() diff --git a/src/codeflare_sdk/vendored/examples/use-director.py b/src/codeflare_sdk/vendored/examples/use-director.py new file mode 100644 index 00000000..2608c154 --- /dev/null +++ b/src/codeflare_sdk/vendored/examples/use-director.py @@ -0,0 +1,98 @@ +import sys +import os +from os import path +import json +import time + +""" +in case you are working directly with the source, and don't wish to +install the module with pip install, you can directly import the packages by uncommenting the following code. +""" + +""" +sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) + +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) +sibling_dirs = [ + d for d in os.listdir(parent_dir) if os.path.isdir(os.path.join(parent_dir, d)) +] +for sibling_dir in sibling_dirs: + sys.path.append(os.path.join(parent_dir, sibling_dir)) + +""" +from codeflare_sdk.vendored.python_client import kuberay_cluster_api + +from codeflare_sdk.vendored.python_client.utils import kuberay_cluster_builder + + +def wait(duration: int = 5, step_name: str = "next"): + print("waiting for {} seconds before {} step".format(duration, step_name)) + for i in range(duration, 0, -1): + sys.stdout.write(str(i) + " ") + sys.stdout.flush() + time.sleep(1) + print() + + +def main(): + print("starting cluster handler...") + + my_kube_ray_api = kuberay_cluster_api.RayClusterApi() + + my_cluster_director = kuberay_cluster_builder.Director() + + # building the raycluster representation + cluster_body = my_cluster_director.build_small_cluster( + name="new-small-cluster", k8s_namespace="default" + ) + + # creating the raycluster in k8s + if cluster_body: + print("creating the cluster...") + my_kube_ray_api.create_ray_cluster(body=cluster_body) + + # now the cluster should be created. + # the rest of the code is simply to fetch, print and cleanup the created cluster + + print("fetching the cluster...") + # fetching the raycluster from k8s api-server + kube_ray_cluster = my_kube_ray_api.get_ray_cluster( + name=cluster_body["metadata"]["name"], k8s_namespace="default" + ) + + if kube_ray_cluster: + print( + "try: kubectl -n {} get raycluster {} -o yaml".format( + kube_ray_cluster["metadata"]["namespace"], + kube_ray_cluster["metadata"]["name"], + ) + ) + wait(step_name="print created cluster in JSON") + print("printing the raycluster JSON representation...") + json_formatted_str = json.dumps(kube_ray_cluster, indent=2) + print(json_formatted_str) + + # waiting until the cluster is running, and has its status updated + is_running = my_kube_ray_api.wait_until_ray_cluster_running( + name=kube_ray_cluster["metadata"]["name"], + k8s_namespace=kube_ray_cluster["metadata"]["namespace"], + ) + + print( + "raycluster {} status is {}".format( + kube_ray_cluster["metadata"]["name"], "Running" if is_running else "unknown" + ) + ) + + wait(step_name="cleaning up") + print("deleting raycluster {}.".format(kube_ray_cluster["metadata"]["name"])) + + my_kube_ray_api.delete_ray_cluster( + name=kube_ray_cluster["metadata"]["name"], + k8s_namespace=kube_ray_cluster["metadata"]["namespace"], + ) + + +if __name__ == "__main__": + main() diff --git a/src/codeflare_sdk/vendored/examples/use-raw-config_map_with-api.py b/src/codeflare_sdk/vendored/examples/use-raw-config_map_with-api.py new file mode 100644 index 00000000..97ac6a57 --- /dev/null +++ b/src/codeflare_sdk/vendored/examples/use-raw-config_map_with-api.py @@ -0,0 +1,213 @@ +import json +from os import path +import os +import sys +import time +from kubernetes.client.rest import ApiException +from kubernetes import client +from kubernetes.stream import stream + + +""" +in case you are working directly with the source, and don't wish to +install the module with pip install, you can directly import the packages by uncommenting the following code. +""" + +""" +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) +sibling_dirs = [ + d for d in os.listdir(parent_dir) if os.path.isdir(os.path.join(parent_dir, d)) +] +for sibling_dir in sibling_dirs: + sys.path.append(os.path.join(parent_dir, sibling_dir)) +""" +from codeflare_sdk.vendored.python_client import kuberay_cluster_api + + +configmap_body: dict = { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": {"name": "ray-code-single"}, + "data": { + "sample_code.py": 'import ray\nfrom os import environ\nredis_pass = environ.get("REDIS_PASSWORD") \nprint("trying to connect to Ray!")\nray.init(address="auto", _redis_password=redis_pass)\nprint("now executing some code with Ray!")\nimport time\nstart = time.time()\n@ray.remote\ndef f():\n time.sleep(0.01)\n return ray._private.services.get_node_ip_address()\nvalues=set(ray.get([f.remote() for _ in range(1000)]))\nprint("Ray Nodes: ",str(values))\nfile = open("/tmp/ray_nodes.txt","a")\nfile.write("available nodes: %s\\n" % str(values))\nfile.close()\nend = time.time()\nprint("Execution time = ",end - start)\n' + }, +} + +cluster_body: dict = { + "apiVersion": "ray.io/v1", + "kind": "RayCluster", + "metadata": { + "labels": {"controller-tools.k8s.io": "1.0", "demo-cluster": "yes"}, + "name": "raycluster-getting-started", + }, + "spec": { + "rayVersion": "2.46.0", + "headGroupSpec": { + "rayStartParams": { + "dashboard-host": "0.0.0.0", + "num-cpus": "2", + }, + "template": { + "spec": { + "containers": [ + { + "name": "ray-head", + "image": "rayproject/ray:2.46.0", + "volumeMounts": [{"mountPath": "/opt", "name": "config"}], + } + ], + "resources": { + "limits": {"cpu": "2", "memory": "3G"}, + "requests": {"cpu": "1500m", "memory": "3G"}, + }, + "volumes": [ + { + "name": "config", + "configMap": { + "name": configmap_body["metadata"]["name"], + "items": [ + {"key": "sample_code.py", "path": "sample_code.py"} + ], + }, + } + ], + } + }, + }, + }, +} + +""" +the following code is simply to create a configmap and a raycluster using the kuberay_cluster_api + +after the cluster is created, the code will execute a python command in the head pod of the cluster + +then it will print the logs of the head pod + +then it will list the clusters and delete the cluster and the configmap +""" + + +def main(): + print("starting cluster handler...") + + my_kube_ray_api = kuberay_cluster_api.RayClusterApi() # creating the api object + + try: + my_kube_ray_api.core_v1_api.create_namespaced_config_map( + "default", configmap_body + ) + + except ApiException as e: + if e.status == 409: + print( + "configmap {} already exists = {} moving on...".format( + configmap_body["metadata"]["name"], e + ) + ) + else: + print("error creating configmap: {}".format(e)) + + # waiting for the configmap tp be created + time.sleep(3) + + my_kube_ray_api.create_ray_cluster(body=cluster_body) # creating the cluster + + # the rest of the code is simply to fetch, print and cleanup the created cluster + kube_ray_cluster = my_kube_ray_api.get_ray_cluster( + name=cluster_body["metadata"]["name"], k8s_namespace="default" + ) + + if kube_ray_cluster: + print("printing the raycluster json representation...") + json_formatted_str = json.dumps(kube_ray_cluster, indent=2) + print(json_formatted_str) + else: + print("Unable to fetch cluster {}".format(cluster_body["metadata"]["name"])) + return + + print( + "try: kubectl -n default get raycluster {} -o yaml".format( + kube_ray_cluster["metadata"]["name"] + ) + ) + # the rest of the code is simply to list and cleanup the created cluster + + time.sleep(3) + try: + pod_list: client.V1PodList = my_kube_ray_api.core_v1_api.list_namespaced_pod( + namespace="default", + label_selector="ray.io/cluster={}".format(cluster_body["metadata"]["name"]), + ) # getting the pods of the cluster + if pod_list != None: + for pod in pod_list.items: + try: + # Calling exec and waiting for response + exec_command = ["python", "/opt/sample_code.py"] + + print( + "executing a Python command in the raycluster: {}".format( + exec_command + ) + ) + # executing a ray command in the head pod + resp = stream( + my_kube_ray_api.core_v1_api.connect_get_namespaced_pod_exec, + pod.metadata.name, + "default", + command=exec_command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + ) + print("Response: " + resp) + + # getting the logs from the pod + time.sleep(3) + print( + "getting the logs from the raycluster pod: {}".format( + pod.metadata.name + ) + ) + api_response = my_kube_ray_api.core_v1_api.read_namespaced_pod_log( + name=pod.metadata.name, namespace="default" + ) + print(api_response) + + except ApiException as e: + print("An exception has ocurred in reading the logs {}".format(e)) + except ApiException as e: + print("An exception has ocurred in listing pods the logs".format(e)) + + kube_ray_list = my_kube_ray_api.list_ray_clusters( + k8s_namespace="default", label_selector="demo-cluster=yes" + ) + + if "items" in kube_ray_list: + for cluster in kube_ray_list["items"]: + print("deleting raycluster = {}".format(cluster["metadata"]["name"])) + my_kube_ray_api.delete_ray_cluster( + name=cluster["metadata"]["name"], + k8s_namespace=cluster["metadata"]["namespace"], + ) # deleting the cluster + + try: + my_kube_ray_api.core_v1_api.delete_namespaced_config_map( + configmap_body["metadata"]["name"], "default" + ) # deleting the configmap + print("deleting configmap: {}".format(configmap_body["metadata"]["name"])) + except ApiException as e: + if e.status == 404: + print( + "configmap = {}, does not exist moving on...".format( + configmap_body["metadata"]["name"], e + ) + ) + else: + print("error deleting configmap: {}".format(e)) + + +if __name__ == "__main__": + main() diff --git a/src/codeflare_sdk/vendored/examples/use-raw-with-api.py b/src/codeflare_sdk/vendored/examples/use-raw-with-api.py new file mode 100644 index 00000000..5ab89586 --- /dev/null +++ b/src/codeflare_sdk/vendored/examples/use-raw-with-api.py @@ -0,0 +1,195 @@ +import json +from os import path +import os +import sys + + +""" +in case you are working directly with the source, and don't wish to +install the module with pip install, you can directly import the packages by uncommenting the following code. +""" + +""" +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) +sibling_dirs = [ + d for d in os.listdir(parent_dir) if os.path.isdir(os.path.join(parent_dir, d)) +] +for sibling_dir in sibling_dirs: + sys.path.append(os.path.join(parent_dir, sibling_dir)) +""" +from codeflare_sdk.vendored.python_client import kuberay_cluster_api + +cluster_body: dict = { + "apiVersion": "ray.io/v1", + "kind": "RayCluster", + "metadata": { + "labels": {"controller-tools.k8s.io": "1.0", "demo-cluster": "yes"}, + "name": "raycluster-mini-raw", + }, + "spec": { + "rayVersion": "2.46.0", + "headGroupSpec": { + "rayStartParams": { + "dashboard-host": "0.0.0.0", + "num-cpus": "1", + }, + "template": { + "spec": { + "containers": [ + { + "name": "ray-head", + "image": "rayproject/ray:2.46.0", + "resources": { + "limits": {"cpu": 1, "memory": "2Gi"}, + "requests": {"cpu": "500m", "memory": "2Gi"}, + }, + "ports": [ + {"containerPort": 6379, "name": "gcs-server"}, + {"containerPort": 8265, "name": "dashboard"}, + {"containerPort": 10001, "name": "client"}, + ], + } + ] + } + }, + }, + }, +} + + +cluster_body2: dict = { + "apiVersion": "ray.io/v1", + "kind": "RayCluster", + "metadata": { + "labels": {"controller-tools.k8s.io": "1.0", "demo-cluster": "yes"}, + "name": "raycluster-complete-raw", + }, + "spec": { + "rayVersion": "2.46.0", + "headGroupSpec": { + "rayStartParams": {"dashboard-host": "0.0.0.0"}, + "template": { + "metadata": {"labels": {}}, + "spec": { + "containers": [ + { + "name": "ray-head", + "image": "rayproject/ray:2.46.0", + "ports": [ + {"containerPort": 6379, "name": "gcs"}, + {"containerPort": 8265, "name": "dashboard"}, + {"containerPort": 10001, "name": "client"}, + ], + "lifecycle": { + "preStop": { + "exec": {"command": ["/bin/sh", "-c", "ray stop"]} + } + }, + "volumeMounts": [ + {"mountPath": "/tmp/ray", "name": "ray-logs"} + ], + "resources": { + "limits": {"cpu": "1", "memory": "2G"}, + "requests": {"cpu": "500m", "memory": "2G"}, + }, + } + ], + "volumes": [{"name": "ray-logs", "emptyDir": {}}], + }, + }, + }, + "workerGroupSpecs": [ + { + "replicas": 1, + "minReplicas": 1, + "maxReplicas": 10, + "groupName": "small-group", + "rayStartParams": {}, + "template": { + "spec": { + "containers": [ + { + "name": "ray-worker", + "image": "rayproject/ray:2.46.0", + "lifecycle": { + "preStop": { + "exec": { + "command": ["/bin/sh", "-c", "ray stop"] + } + } + }, + "volumeMounts": [ + {"mountPath": "/tmp/ray", "name": "ray-logs"} + ], + "resources": { + "limits": {"cpu": "2", "memory": "3G"}, + "requests": {"cpu": "1500m", "memory": "3G"}, + }, + } + ], + "volumes": [{"name": "ray-logs", "emptyDir": {}}], + } + }, + } + ], + }, +} + + +def main(): + print("starting cluster handler...") + + my_kube_ray_api = kuberay_cluster_api.RayClusterApi() + + my_kube_ray_api.create_ray_cluster(body=cluster_body) + + my_kube_ray_api.create_ray_cluster(body=cluster_body2) + + # the rest of the code is simply to fetch, print and cleanup the created cluster + kube_ray_cluster = my_kube_ray_api.get_ray_cluster( + name=cluster_body["metadata"]["name"], k8s_namespace="default" + ) + + if kube_ray_cluster: + print("printing the raycluster json representation...") + json_formatted_str = json.dumps(kube_ray_cluster, indent=2) + print(json_formatted_str) + else: + print("Unable to fetch cluster {}".format(cluster_body["metadata"]["name"])) + return + + print( + "try: kubectl -n default get raycluster {} -o yaml".format( + kube_ray_cluster["metadata"]["name"] + ) + ) + # the rest of the code is simply to list and cleanup the created cluster + kube_ray_list = my_kube_ray_api.list_ray_clusters( + k8s_namespace="default", label_selector="demo-cluster=yes" + ) + if "items" in kube_ray_list: + line = "-" * 72 + print(line) + print("{:<63s}{:>2s}".format("Name", "Namespace")) + print(line) + for cluster in kube_ray_list["items"]: + print( + "{:<63s}{:>2s}".format( + cluster["metadata"]["name"], + cluster["metadata"]["namespace"], + ) + ) + print(line) + + if "items" in kube_ray_list: + for cluster in kube_ray_list["items"]: + print("deleting raycluster = {}".format(cluster["metadata"]["name"])) + my_kube_ray_api.delete_ray_cluster( + name=cluster["metadata"]["name"], + k8s_namespace=cluster["metadata"]["namespace"], + ) + + +if __name__ == "__main__": + main() diff --git a/src/codeflare_sdk/vendored/examples/use-utils.py b/src/codeflare_sdk/vendored/examples/use-utils.py new file mode 100644 index 00000000..ab3e3736 --- /dev/null +++ b/src/codeflare_sdk/vendored/examples/use-utils.py @@ -0,0 +1,117 @@ +import sys +import os +from os import path +import json + + +""" +in case you are working directly with the source, and don't wish to +install the module with pip install, you can directly import the packages by uncommenting the following code. +""" + +""" +sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) + +current_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) +sibling_dirs = [ + d for d in os.listdir(parent_dir) if os.path.isdir(os.path.join(parent_dir, d)) +] +for sibling_dir in sibling_dirs: + sys.path.append(os.path.join(parent_dir, sibling_dir)) +""" + +from codeflare_sdk.vendored.python_client import kuberay_cluster_api + +from codeflare_sdk.vendored.python_client.utils import ( + kuberay_cluster_utils, + kuberay_cluster_builder, +) + + +def main(): + print("starting cluster handler...") + my_kuberay_api = kuberay_cluster_api.RayClusterApi() # this is the main api object + + my_cluster_builder = ( + kuberay_cluster_builder.ClusterBuilder() + ) # this is the builder object, to create a cluster with a more granular control + + my_Cluster_utils = ( + kuberay_cluster_utils.ClusterUtils() + ) # this is the utils object, to perform operations on a cluster + + cluster1 = ( + my_cluster_builder.build_meta( + name="new-cluster1", labels={"demo-cluster": "yes"} + ) + .build_head() + .build_worker(group_name="workers") + .get_cluster() + ) # this is the cluster object, it is a dict + + if not my_cluster_builder.succeeded: + print("error building the cluster, aborting...") + return + + print("creating raycluster = {}".format(cluster1["metadata"]["name"])) + my_kuberay_api.create_ray_cluster( + body=cluster1 + ) # this is the api call to create the cluster1 in k8s + + cluster_to_patch, succeeded = my_Cluster_utils.update_worker_group_replicas( + cluster1, group_name="workers", max_replicas=4, min_replicas=1, replicas=2 + ) + + if succeeded: + print( + "trying to patch raycluster = {}".format( + cluster_to_patch["metadata"]["name"] + ) + ) + my_kuberay_api.patch_ray_cluster( + name=cluster_to_patch["metadata"]["name"], ray_patch=cluster_to_patch + ) # this is the api call to patch the cluster1 in k8s + + cluster_to_patch, succeeded = my_Cluster_utils.duplicate_worker_group( + cluster1, group_name="workers", new_group_name="duplicate-workers" + ) # this is the api call to duplicate the worker group in the cluster1 + if succeeded: + print( + "trying to patch raycluster = {}".format( + cluster_to_patch["metadata"]["name"] + ) + ) + my_kuberay_api.patch_ray_cluster( + name=cluster_to_patch["metadata"]["name"], ray_patch=cluster_to_patch + ) + + # the rest of the code is simply to list and cleanup the created cluster + kube_ray_list = my_kuberay_api.list_ray_clusters( + k8s_namespace="default", label_selector="demo-cluster=yes" + ) # this is the api call to list the clusters in k8s + if "items" in kube_ray_list: + line = "-" * 72 + print(line) + print("{:<63s}{:>2s}".format("Name", "Namespace")) + print(line) + for cluster in kube_ray_list["items"]: + print( + "{:<63s}{:>2s}".format( + cluster["metadata"]["name"], + cluster["metadata"]["namespace"], + ) + ) + print(line) + + if "items" in kube_ray_list: + for cluster in kube_ray_list["items"]: + print("deleting raycluster = {}".format(cluster["metadata"]["name"])) + my_kuberay_api.delete_ray_cluster( + name=cluster["metadata"]["name"], + k8s_namespace=cluster["metadata"]["namespace"], + ) # this is the api call to delete the cluster in k8s + + +if __name__ == "__main__": + main() diff --git a/src/codeflare_sdk/vendored/poetry.lock b/src/codeflare_sdk/vendored/poetry.lock new file mode 100644 index 00000000..b8d82ccc --- /dev/null +++ b/src/codeflare_sdk/vendored/poetry.lock @@ -0,0 +1,439 @@ +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. + +[[package]] +name = "cachetools" +version = "5.5.2" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, + {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, +] + +[[package]] +name = "certifi" +version = "2025.6.15" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"}, + {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, +] + +[[package]] +name = "durationpy" +version = "0.10" +description = "Module for converting between datetime.timedelta and Go's Duration strings." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286"}, + {file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"}, +] + +[[package]] +name = "google-auth" +version = "2.40.3" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca"}, + {file = "google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyjwt = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0)"] +testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +urllib3 = ["packaging", "urllib3"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "kubernetes" +version = "33.1.0" +description = "Kubernetes python client" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "kubernetes-33.1.0-py2.py3-none-any.whl", hash = "sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5"}, + {file = "kubernetes-33.1.0.tar.gz", hash = "sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993"}, +] + +[package.dependencies] +certifi = ">=14.05.14" +durationpy = ">=0.7" +google-auth = ">=1.0.1" +oauthlib = ">=3.2.2" +python-dateutil = ">=2.5.3" +pyyaml = ">=5.4.1" +requests = "*" +requests-oauthlib = "*" +six = ">=1.9.0" +urllib3 = ">=1.24.2" +websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" + +[package.extras] +adal = ["adal (>=1.0.2)"] + +[[package]] +name = "oauthlib" +version = "3.3.1" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, + {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, +] + +[package.dependencies] +pyasn1 = ">=0.6.1,<0.7.0" + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "requests" +version = "2.32.4" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +groups = ["main"] +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "rsa" +version = "4.9.1" +description = "Pure-Python RSA implementation" +optional = false +python-versions = "<4,>=3.6" +groups = ["main"] +files = [ + {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, + {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.11" +content-hash = "b36691a561f80767983438fab17d96ee0064eac943f5055abc2dcfee84c07dd7" diff --git a/src/codeflare_sdk/vendored/pyproject.toml b/src/codeflare_sdk/vendored/pyproject.toml new file mode 100755 index 00000000..916829ba --- /dev/null +++ b/src/codeflare_sdk/vendored/pyproject.toml @@ -0,0 +1,26 @@ +[tool.poetry] +name = "python-client" +version = "0.0.0-dev" +description = "Python Client for Kuberay" +license = "Apache-2.0" + +readme = "README.md" +repository = "https://github.com/ray-project/kuberay" +homepage = "https://github.com/ray-project/kuberay" +keywords = ["kuberay", "python", "client"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent" +] +packages = [ + { include = "python_client" } +] + +[tool.poetry.dependencies] +python = "^3.11" +kubernetes = ">=25.0.0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/codeflare_sdk/vendored/python_client/__init__.py b/src/codeflare_sdk/vendored/python_client/__init__.py new file mode 100644 index 00000000..6849410a --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client/__init__.py @@ -0,0 +1 @@ +__version__ = "1.1.0" diff --git a/src/codeflare_sdk/vendored/python_client/constants.py b/src/codeflare_sdk/vendored/python_client/constants.py new file mode 100644 index 00000000..d47e270d --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client/constants.py @@ -0,0 +1,13 @@ +# Declares the constants that are used by the client +import logging + +# Group, Version, Plural +GROUP = "ray.io" +CLUSTER_VERSION = "v1" +JOB_VERSION = "v1" +CLUSTER_PLURAL = "rayclusters" +JOB_PLURAL = "rayjobs" +CLUSTER_KIND = "RayCluster" +JOB_KIND = "RayJob" +# log level +LOGLEVEL = logging.INFO diff --git a/src/codeflare_sdk/vendored/python_client/kuberay_cluster_api.py b/src/codeflare_sdk/vendored/python_client/kuberay_cluster_api.py new file mode 100644 index 00000000..8307cdda --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client/kuberay_cluster_api.py @@ -0,0 +1,311 @@ +""" +Set of APIs to manage rayclusters. +""" + +__copyright__ = "Copyright 2021, Microsoft Corp." + +import logging +import time +from kubernetes import client, config +from kubernetes.client.rest import ApiException +from typing import Any, Optional +from codeflare_sdk.vendored.python_client import constants + + +log = logging.getLogger(__name__) +if logging.getLevelName(log.level) == "NOTSET": + logging.basicConfig(format="%(asctime)s %(message)s", level=constants.LOGLEVEL) + + +class RayClusterApi: + """ + RayClusterApi provides APIs to list, get, create, build, update, delete rayclusters. + + Methods: + - list_ray_clusters(k8s_namespace: str = "default", async_req: bool = False) -> Any: + - get_ray_cluster(name: str, k8s_namespace: str = "default") -> Any: + - create_ray_cluster(body: Any, k8s_namespace: str = "default") -> Any: + - delete_ray_cluster(name: str, k8s_namespace: str = "default") -> bool: + - patch_ray_cluster(name: str, ray_patch: Any, k8s_namespace: str = "default") -> Any: + """ + + # initial config to setup the kube client + def __init__(self): + # loading the config + try: + self.kube_config: Optional[Any] = config.load_kube_config() + except config.ConfigException: + # No kubeconfig found, try in-cluster config + try: + self.kube_config: Optional[Any] = config.load_incluster_config() + except config.ConfigException: + log.error("Failed to load both kubeconfig and in-cluster config") + raise + + self.api = client.CustomObjectsApi() + self.core_v1_api = client.CoreV1Api() + + def __del__(self): + self.api = None + self.kube_config = None + + def list_ray_clusters( + self, + k8s_namespace: str = "default", + label_selector: str = "", + async_req: bool = False, + ) -> Any: + """List Ray clusters in a given namespace. + + Parameters: + - k8s_namespace (str, optional): The namespace in which to list the Ray clusters. Defaults to "default". + - async_req (bool, optional): Whether to make the request asynchronously. Defaults to False. + + Returns: + Any: The custom resource for Ray clusters in the specified namespace, or None if not found. + + Raises: + ApiException: If there was an error fetching the custom resource. + """ + try: + resource: Any = self.api.list_namespaced_custom_object( + group=constants.GROUP, + version=constants.CLUSTER_VERSION, + plural=constants.CLUSTER_PLURAL, + namespace=k8s_namespace, + label_selector=label_selector, + async_req=async_req, + ) + if "items" in resource: + return resource + return None + except ApiException as e: + if e.status == 404: + log.error("raycluster resource is not found. error = {}".format(e)) + return None + else: + log.error("error fetching custom resource: {}".format(e)) + return None + + def get_ray_cluster(self, name: str, k8s_namespace: str = "default") -> Any: + """Get a specific Ray cluster in a given namespace. + + Parameters: + - name (str): The name of the Ray cluster custom resource. Defaults to "". + - k8s_namespace (str, optional): The namespace in which to retrieve the Ray cluster. Defaults to "default". + + Returns: + Any: The custom resource for the specified Ray cluster, or None if not found. + + Raises: + ApiException: If there was an error fetching the custom resource. + """ + try: + resource: Any = self.api.get_namespaced_custom_object( + group=constants.GROUP, + version=constants.CLUSTER_VERSION, + plural=constants.CLUSTER_PLURAL, + name=name, + namespace=k8s_namespace, + ) + return resource + except ApiException as e: + if e.status == 404: + log.error("raycluster resource is not found. error = {}".format(e)) + return None + else: + log.error("error fetching custom resource: {}".format(e)) + return None + + def get_ray_cluster_status( + self, + name: str, + k8s_namespace: str = "default", + timeout: int = 60, + delay_between_attempts: int = 5, + ) -> Any: + """Get a specific Ray cluster status in a given namespace. + + This method waits until the cluster has a status field populated by the operator. + + Parameters: + - name (str): The name of the Ray cluster custom resource. + - k8s_namespace (str, optional): The namespace in which to retrieve the Ray cluster. Defaults to "default". + - timeout (int, optional): The duration in seconds after which we stop trying to get status. Defaults to 60 seconds. + - delay_between_attempts (int, optional): The duration in seconds to wait between attempts. Defaults to 5 seconds. + + Returns: + Any: The custom resource status for the specified Ray cluster, or None if not found or timeout. + """ + while timeout > 0: + try: + resource: Any = self.api.get_namespaced_custom_object_status( + group=constants.GROUP, + version=constants.CLUSTER_VERSION, + plural=constants.CLUSTER_PLURAL, + name=name, + namespace=k8s_namespace, + ) + except ApiException as e: + if e.status == 404: + log.error("raycluster resource is not found. error = {}".format(e)) + return None + else: + log.error("error fetching custom resource: {}".format(e)) + return None + + if resource and "status" in resource and resource["status"]: + return resource["status"] + else: + log.info("raycluster {} status not set yet, waiting...".format(name)) + time.sleep(delay_between_attempts) + timeout -= delay_between_attempts + + log.info("timed out waiting for raycluster {} status".format(name)) + return None + + def wait_until_ray_cluster_running( + self, + name: str, + k8s_namespace: str = "default", + timeout: int = 60, + delay_between_attempts: int = 5, + ) -> bool: + """Wait until a Ray cluster is in ready state. + + This method waits for the cluster to have a state field with value 'ready'. + + Parameters: + - name (str): The name of the Ray cluster custom resource. + - k8s_namespace (str, optional): The namespace in which to retrieve the Ray cluster. Defaults to "default". + - timeout (int, optional): The duration in seconds after which we stop trying. Defaults to 60 seconds. + - delay_between_attempts (int, optional): The duration in seconds to wait between attempts. Defaults to 5 seconds. + + Returns: + bool: True if the raycluster status is 'ready', False otherwise. + """ + while timeout > 0: + status = self.get_ray_cluster_status( + name, k8s_namespace, timeout, delay_between_attempts + ) + + if status and "state" in status: + current_state = status["state"] + if current_state == "ready": + log.info( + "raycluster {} is ready with state: {}".format( + name, current_state + ) + ) + return True + else: + log.info( + "raycluster {} is in state: {} (waiting for ready)".format( + name, current_state + ) + ) + else: + log.info( + "raycluster {} state field not available yet, waiting...".format( + name + ) + ) + + time.sleep(delay_between_attempts) + timeout -= delay_between_attempts + + log.info("raycluster {} has not become ready before timeout".format(name)) + return False + + def create_ray_cluster(self, body: Any, k8s_namespace: str = "default") -> Any: + """Create a new Ray cluster custom resource. + + Parameters: + - body (Any): The data of the custom resource to create. + - k8s_namespace (str, optional): The namespace in which to create the custom resource. Defaults to "default". + + Returns: + Any: The created custom resource, or None if it already exists or there was an error. + """ + try: + resource: Any = self.api.create_namespaced_custom_object( + group=constants.GROUP, + version=constants.CLUSTER_VERSION, + plural=constants.CLUSTER_PLURAL, + body=body, + namespace=k8s_namespace, + ) + return resource + except ApiException as e: + if e.status == 409: + log.error( + "raycluster resource already exists. error = {}".format(e.reason) + ) + return None + else: + log.error("error creating custom resource: {}".format(e)) + return None + + def delete_ray_cluster(self, name: str, k8s_namespace: str = "default") -> bool: + """Delete a Ray cluster custom resource. + + Parameters: + - name (str): The name of the Ray cluster custom resource to delete. + - k8s_namespace (str, optional): The namespace in which the Ray cluster exists. Defaults to "default". + + Returns: + Any: The deleted custom resource, or None if already deleted or there was an error. + """ + try: + resource: Any = self.api.delete_namespaced_custom_object( + group=constants.GROUP, + version=constants.CLUSTER_VERSION, + plural=constants.CLUSTER_PLURAL, + name=name, + namespace=k8s_namespace, + ) + return resource + except ApiException as e: + if e.status == 404: + log.error( + "raycluster custom resource already deleted. error = {}".format( + e.reason + ) + ) + return None + else: + log.error( + "error deleting the raycluster custom resource: {}".format(e.reason) + ) + return None + + def patch_ray_cluster( + self, name: str, ray_patch: Any, k8s_namespace: str = "default" + ) -> Any: + """Patch an existing Ray cluster custom resource. + + Parameters: + - name (str): The name of the Ray cluster custom resource to be patched. + - ray_patch (Any): The patch data for the Ray cluster. + - k8s_namespace (str, optional): The namespace in which the Ray cluster exists. Defaults to "default". + + Returns: + bool: True if the patch was successful, False otherwise. + """ + try: + # we patch the existing raycluster with the new config + self.api.patch_namespaced_custom_object( + group=constants.GROUP, + version=constants.CLUSTER_VERSION, + plural=constants.CLUSTER_PLURAL, + name=name, + body=ray_patch, + namespace=k8s_namespace, + ) + except ApiException as e: + log.error("raycluster `{}` failed to patch, with error: {}".format(name, e)) + return False + else: + log.info("raycluster `%s` is patched successfully", name) + + return True diff --git a/src/codeflare_sdk/vendored/python_client/kuberay_job_api.py b/src/codeflare_sdk/vendored/python_client/kuberay_job_api.py new file mode 100644 index 00000000..d2d1d7e0 --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client/kuberay_job_api.py @@ -0,0 +1,381 @@ +""" +Set of APIs to manage rayjobs. +""" + +import logging +import time +from kubernetes import client, config +from kubernetes.client.rest import ApiException +from typing import Any, Optional +from codeflare_sdk.vendored.python_client import constants + + +log = logging.getLogger(__name__) +if logging.getLevelName(log.level) == "NOTSET": + logging.basicConfig(format="%(asctime)s %(message)s", level=constants.LOGLEVEL) + +TERMINAL_JOB_STATUSES = [ + "STOPPED", + "SUCCEEDED", + "FAILED", +] + + +class RayjobApi: + """ + RayjobApi provides APIs to list, get, create, build, update, delete rayjobs. + Methods: + - submit_job(k8s_namespace: str, job: Any) -> Any: Submit and execute a job asynchronously. + - suspend_job(name: str, k8s_namespace: str) -> bool: Stop a job by suspending it. + - resubmit_job(name: str, k8s_namespace: str) -> bool: Resubmit a job that has been suspended. + - get_job(name: str, k8s_namespace: str) -> Any: Get a job. + - list_jobs(k8s_namespace: str) -> Any: List all jobs. + - get_job_status(name: str, k8s_namespace: str, timeout: int, delay_between_attempts: int) -> Any: Get the most recent status of a job. + - wait_until_job_finished(name: str, k8s_namespace: str, timeout: int, delay_between_attempts: int) -> bool: Wait until a job is completed. + - wait_until_job_running(name: str, k8s_namespace: str, timeout: int, delay_between_attempts: int) -> bool: Wait until a job reaches running state. + - delete_job(name: str, k8s_namespace: str) -> bool: Delete a job and all of its associated data. + """ + + # initial config to setup the kube client + def __init__(self): + # loading the config + try: + self.kube_config: Optional[Any] = config.load_kube_config() + except config.ConfigException: + # No kubeconfig found, try in-cluster config + try: + self.kube_config: Optional[Any] = config.load_incluster_config() + except config.ConfigException: + log.error("Failed to load both kubeconfig and in-cluster config") + raise + + self.api = client.CustomObjectsApi() + + def __del__(self): + self.api = None + self.kube_config = None + + def submit_job(self, k8s_namespace: str = "default", job: Any = None) -> Any: + """Submit a Ray job to a given namespace.""" + try: + rayjob = self.api.create_namespaced_custom_object( + group=constants.GROUP, + version=constants.JOB_VERSION, + plural=constants.JOB_PLURAL, + body=job, + namespace=k8s_namespace, + ) + return rayjob + except ApiException as e: + log.error("error submitting ray job: {}".format(e)) + return None + + def get_job_status( + self, + name: str, + k8s_namespace: str = "default", + timeout: int = 60, + delay_between_attempts: int = 5, + ) -> Any: + """Get a specific Ray job status in a given namespace. + + This method waits until the job has a status field populated by the operator. + + Parameters: + - name (str): The name of the Ray job custom resource. + - k8s_namespace (str, optional): The namespace in which to retrieve the Ray job. Defaults to "default". + - timeout (int, optional): The duration in seconds after which we stop trying to get status. Defaults to 60 seconds. + - delay_between_attempts (int, optional): The duration in seconds to wait between attempts. Defaults to 5 seconds. + + Returns: + Any: The custom resource status for the specified Ray job, or None if not found or timeout. + """ + while timeout > 0: + try: + resource: Any = self.api.get_namespaced_custom_object_status( + group=constants.GROUP, + version=constants.JOB_VERSION, + plural=constants.JOB_PLURAL, + name=name, + namespace=k8s_namespace, + ) + except ApiException as e: + if e.status == 404: + log.error("rayjob resource is not found. error = {}".format(e)) + return None + else: + log.error("error fetching custom resource: {}".format(e)) + return None + + if resource and "status" in resource and resource["status"]: + return resource["status"] + else: + log.info("rayjob {} status not set yet, waiting...".format(name)) + time.sleep(delay_between_attempts) + timeout -= delay_between_attempts + + log.info("rayjob {} status not set yet, timing out...".format(name)) + return None + + def wait_until_job_finished( + self, + name: str, + k8s_namespace: str = "default", + timeout: int = 60, + delay_between_attempts: int = 5, + ) -> bool: + """Wait until a Ray job reaches a terminal status. + + This method waits for the job to reach a terminal state by checking both jobStatus + (STOPPED, SUCCEEDED, FAILED) and jobDeploymentStatus (Complete, Failed). + + Parameters: + - name (str): The name of the Ray job custom resource. + - k8s_namespace (str, optional): The namespace in which to retrieve the Ray job. Defaults to "default". + - timeout (int, optional): The duration in seconds after which we stop trying. Defaults to 60 seconds. + - delay_between_attempts (int, optional): The duration in seconds to wait between attempts. Defaults to 5 seconds. + + Returns: + bool: True if the rayjob reaches a terminal status, False otherwise. + """ + while timeout > 0: + status = self.get_job_status( + name, k8s_namespace, timeout, delay_between_attempts + ) + + if status: + if "jobDeploymentStatus" in status: + deployment_status = status["jobDeploymentStatus"] + if deployment_status in ["Complete", "Failed"]: + log.info( + "rayjob {} has finished with deployment status: {}".format( + name, deployment_status + ) + ) + return True + elif deployment_status == "Suspended": + log.info("rayjob {} is suspended".format(name)) + # Suspended is not terminal, continue waiting + elif deployment_status in ["Initializing", "Running", "Suspending"]: + log.info( + "rayjob {} is {}".format(name, deployment_status.lower()) + ) + elif deployment_status: + log.info( + "rayjob {} deployment status: {}".format( + name, deployment_status + ) + ) + + if "jobStatus" in status: + current_status = status["jobStatus"] + if current_status in ["", "PENDING"]: + log.info("rayjob {} has not started yet".format(name)) + elif current_status == "RUNNING": + log.info("rayjob {} is running".format(name)) + elif current_status in TERMINAL_JOB_STATUSES: + log.info( + "rayjob {} has finished with status {}!".format( + name, current_status + ) + ) + return True + else: + log.info( + "rayjob {} has an unknown status: {}".format( + name, current_status + ) + ) + elif "jobDeploymentStatus" not in status: + log.info( + "rayjob {} status fields not available yet, waiting...".format( + name + ) + ) + + time.sleep(delay_between_attempts) + timeout -= delay_between_attempts + + log.info( + "rayjob {} has not reached terminal status before timeout".format(name) + ) + return False + + def wait_until_job_running( + self, + name: str, + k8s_namespace: str = "default", + timeout: int = 60, + delay_between_attempts: int = 5, + ) -> bool: + """Wait until a Ray job reaches Running state. + + This method waits for the job's jobDeploymentStatus to reach "Running". + Useful for confirming a job has started after submission or resubmission. + + Parameters: + - name (str): The name of the Ray job custom resource. + - k8s_namespace (str, optional): The namespace in which to retrieve the Ray job. Defaults to "default". + - timeout (int, optional): The duration in seconds after which we stop trying. Defaults to 60 seconds. + - delay_between_attempts (int, optional): The duration in seconds to wait between attempts. Defaults to 5 seconds. + + Returns: + bool: True if the rayjob reaches Running status, False otherwise. + """ + while timeout > 0: + status = self.get_job_status( + name, k8s_namespace, timeout, delay_between_attempts + ) + + if status and "jobDeploymentStatus" in status: + deployment_status = status["jobDeploymentStatus"] + if deployment_status == "Running": + log.info("rayjob {} is running".format(name)) + return True + elif deployment_status in ["Complete", "Failed", "Suspended"]: + log.info( + "rayjob {} reached terminal/suspended status {} before running".format( + name, deployment_status + ) + ) + return False + elif deployment_status: + log.info("rayjob {} is {}".format(name, deployment_status.lower())) + else: + log.info("rayjob {} deployment status not set yet".format(name)) + else: + log.info("rayjob {} status not available yet, waiting...".format(name)) + + time.sleep(delay_between_attempts) + timeout -= delay_between_attempts + + log.info("rayjob {} has not reached running status before timeout".format(name)) + return False + + def suspend_job(self, name: str, k8s_namespace: str = "default") -> bool: + """Stop a Ray job by setting the suspend field to True. + + This will delete the associated RayCluster and transition the job to 'Suspended' status. + Only works on jobs in 'Running' or 'Initializing' status. + + Parameters: + - name (str): The name of the Ray job custom resource. + - k8s_namespace (str, optional): The namespace in which to stop the Ray job. Defaults to "default". + + Returns: + bool: True if the job was successfully suspended, False otherwise. + """ + try: + patch_body = {"spec": {"suspend": True}} + self.api.patch_namespaced_custom_object( + group=constants.GROUP, + version=constants.JOB_VERSION, + plural=constants.JOB_PLURAL, + name=name, + namespace=k8s_namespace, + body=patch_body, + ) + log.info( + f"Successfully suspended rayjob {name} in namespace {k8s_namespace}" + ) + return True + except ApiException as e: + if e.status == 404: + log.error(f"rayjob {name} not found in namespace {k8s_namespace}") + else: + log.error(f"error stopping rayjob {name}: {e.reason}") + return False + + def resubmit_job(self, name: str, k8s_namespace: str = "default") -> bool: + """Resubmit a suspended Ray job by setting the suspend field to False. + + This will create a new RayCluster and resubmit the job. + Only works on jobs in 'Suspended' status. + + Parameters: + - name (str): The name of the Ray job custom resource. + - k8s_namespace (str, optional): The namespace in which to resubmit the Ray job. Defaults to "default". + + Returns: + bool: True if the job was successfully resubmitted, False otherwise. + """ + try: + # Patch the RayJob to set suspend=false + patch_body = {"spec": {"suspend": False}} + self.api.patch_namespaced_custom_object( + group=constants.GROUP, + version=constants.JOB_VERSION, + plural=constants.JOB_PLURAL, + name=name, + namespace=k8s_namespace, + body=patch_body, + ) + log.info( + f"Successfully resubmitted rayjob {name} in namespace {k8s_namespace}" + ) + return True + except ApiException as e: + if e.status == 404: + log.error(f"rayjob {name} not found in namespace {k8s_namespace}") + else: + log.error(f"error resubmitting rayjob {name}: {e.reason}") + return False + + def delete_job(self, name: str, k8s_namespace: str = "default") -> bool: + """Delete a Ray job and all of its associated data. + + Parameters: + - name (str): The name of the Ray job custom resource. + - k8s_namespace (str, optional): The namespace in which to delete the Ray job. Defaults to "default". + + Returns: + bool: True if the job was successfully deleted, False otherwise. + """ + try: + self.api.delete_namespaced_custom_object( + group=constants.GROUP, + version=constants.JOB_VERSION, + plural=constants.JOB_PLURAL, + name=name, + namespace=k8s_namespace, + ) + log.info(f"Successfully deleted rayjob {name} in namespace {k8s_namespace}") + return True + except ApiException as e: + if e.status == 404: + log.error(f"rayjob custom resource already deleted. error = {e.reason}") + return False + else: + log.error(f"error deleting the rayjob custom resource: {e.reason}") + return False + + def get_job(self, name: str, k8s_namespace: str = "default") -> Any: + """Get a Ray job in a given namespace.""" + try: + return self.api.get_namespaced_custom_object( + group=constants.GROUP, + version=constants.JOB_VERSION, + plural=constants.JOB_PLURAL, + name=name, + namespace=k8s_namespace, + ) + except ApiException as e: + if e.status == 404: + log.error(f"rayjob {name} not found in namespace {k8s_namespace}") + return None + else: + log.error(f"error fetching rayjob {name}: {e.reason}") + return None + + def list_jobs(self, k8s_namespace: str = "default") -> Any: + """List all Ray jobs in a given namespace.""" + try: + return self.api.list_namespaced_custom_object( + group=constants.GROUP, + version=constants.JOB_VERSION, + plural=constants.JOB_PLURAL, + namespace=k8s_namespace, + ) + except ApiException as e: + log.error(f"error fetching rayjobs: {e.reason}") + return None diff --git a/src/codeflare_sdk/vendored/python_client/utils/__init__.py b/src/codeflare_sdk/vendored/python_client/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/codeflare_sdk/vendored/python_client/utils/kuberay_cluster_builder.py b/src/codeflare_sdk/vendored/python_client/utils/kuberay_cluster_builder.py new file mode 100644 index 00000000..be0a66e5 --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client/utils/kuberay_cluster_builder.py @@ -0,0 +1,326 @@ +""" +Set of helper methods to manage rayclusters. Requires Python 3.9 and higher +""" + +import copy +import logging +import math +from typing import Any +from abc import ABCMeta, abstractmethod +from codeflare_sdk.vendored.python_client.utils import kuberay_cluster_utils +from codeflare_sdk.vendored.python_client import constants + + +log = logging.getLogger(__name__) +if logging.getLevelName(log.level) == "NOTSET": + logging.basicConfig(format="%(asctime)s %(message)s", level=constants.LOGLEVEL) + + +class IClusterBuilder(metaclass=ABCMeta): + """ + IClusterBuilder is an interface for building a cluster. + + The class defines abstract methods for building the metadata, head pod, worker groups, and retrieving the built cluster. + """ + + @staticmethod + @abstractmethod + def build_meta(): + "builds the cluster metadata" + + @staticmethod + @abstractmethod + def build_head(): + "builds the head pod" + + @staticmethod + @abstractmethod + def build_worker(): + "builds a worker group" + + @staticmethod + @abstractmethod + def get_cluster(): + "Returns the built cluster" + + +# Concrete implementation of the builder interface +class ClusterBuilder(IClusterBuilder): + """ + ClusterBuilder implements the abstract methods of IClusterBuilder to build a cluster. + """ + + def __init__(self): + self.cluster: dict[str, Any] = {} + self.succeeded: bool = False + self.cluster_utils = kuberay_cluster_utils.ClusterUtils() + + def build_meta( + self, + name: str, + k8s_namespace: str = "default", + labels: dict = None, + ray_version: str = "2.46.0", + ): + """Builds the metadata and ray version of the cluster. + + Parameters: + - name (str): The name of the cluster. + - k8s_namespace (str, optional): The namespace in which the Ray cluster exists. Defaults to "default". + - labels (dict, optional): A dictionary of key-value pairs to add as labels to the cluster. Defaults to None. + - ray_version (str, optional): The version of Ray to use for the cluster. Defaults to "2.46.0". + """ + self.cluster = self.cluster_utils.populate_meta( + cluster=self.cluster, + name=name, + k8s_namespace=k8s_namespace, + labels=labels, + ray_version=ray_version, + ) + return self + + def build_head( + self, + ray_image: str = "rayproject/ray:2.46.0", + service_type: str = "ClusterIP", + cpu_requests: str = "2", + memory_requests: str = "3G", + cpu_limits: str = "2", + memory_limits: str = "3G", + ray_start_params: dict = { + "dashboard-host": "0.0.0.0", + }, + ): + """Build head node of the ray cluster. + + Parameters: + - ray_image (str): Docker image for the head node. Default value is "rayproject/ray:2.46.0". + - service_type (str): Service type of the head node. Default value is "ClusterIP", which creates a headless ClusterIP service. + - cpu_requests (str): CPU requests for the head node. Default value is "2". + - memory_requests (str): Memory requests for the head node. Default value is "3G". + - cpu_limits (str): CPU limits for the head node. Default value is "2". + - memory_limits (str): Memory limits for the head node. Default value is "3G". + - ray_start_params (dict): Dictionary of start parameters for the head node. + Default values is "dashboard-host": "0.0.0.0". + """ + self.cluster, self.succeeded = self.cluster_utils.populate_ray_head( + self.cluster, + ray_image=ray_image, + service_type=service_type, + cpu_requests=cpu_requests, + memory_requests=memory_requests, + cpu_limits=cpu_limits, + memory_limits=memory_limits, + ray_start_params=ray_start_params, + ) + return self + + def build_worker( + self, + group_name: str, + ray_image: str = "rayproject/ray:2.46.0", + ray_command: Any = ["/bin/bash", "-lc"], + init_image: str = "busybox:1.28", + cpu_requests: str = "1", + memory_requests: str = "1G", + cpu_limits: str = "2", + memory_limits: str = "2G", + replicas: int = 1, + min_replicas: int = -1, + max_replicas: int = -1, + ray_start_params: dict = {}, + ): + """Build worker specifications of the cluster. + + This function sets the worker configuration of the cluster, including the Docker image, CPU and memory requirements, number of replicas, and other parameters. + + Parameters: + - group_name (str): name of the worker group. + - ray_image (str, optional): Docker image for the Ray process. Default is "rayproject/ray:2.46.0". + - ray_command (Any, optional): Command to run in the Docker container. Default is ["/bin/bash", "-lc"]. + - init_image (str, optional): Docker image for the init container. Default is "busybox:1.28". + - cpu_requests (str, optional): CPU requests for the worker pods. Default is "1". + - memory_requests (str, optional): Memory requests for the worker pods. Default is "1G". + - cpu_limits (str, optional): CPU limits for the worker pods. Default is "2". + - memory_limits (str, optional): Memory limits for the worker pods. Default is "2G". + - replicas (int, optional): Number of worker pods to run. Default is 1. + - min_replicas (int, optional): Minimum number of worker pods to run. Default is -1. + - max_replicas (int, optional): Maximum number of worker pods to run. Default is -1. + - ray_start_params (dict, optional): Additional parameters to pass to the ray start command. Default is {}. + """ + if min_replicas < 0: + min_replicas = int(math.ceil(replicas / 2)) + if max_replicas < 0: + max_replicas = int(replicas * 3) + + if "spec" in self.cluster.keys(): + if "workerGroupSpecs" not in self.cluster.keys(): + log.info( + "setting the workerGroupSpecs for group_name {}".format(group_name) + ) + self.cluster["spec"]["workerGroupSpecs"] = [] + else: + log.error( + "error creating custom resource: {meta}, the spec section is missing, did you run build_head()?".format( + self.cluster["metadata"] + ) + ) + self.succeeded = False + return self + + worker_group, self.succeeded = self.cluster_utils.populate_worker_group( + group_name, + ray_image, + ray_command, + init_image, + cpu_requests, + memory_requests, + cpu_limits, + memory_limits, + replicas, + min_replicas, + max_replicas, + ray_start_params, + ) + + if self.succeeded: + self.cluster["spec"]["workerGroupSpecs"].append(worker_group) + return self + + def get_cluster(self): + cluster = copy.deepcopy(self.cluster) + return cluster + + +class Director: + def __init__(self): + self.cluster_builder = ClusterBuilder() + + def build_basic_cluster( + self, name: str, k8s_namespace: str = "default", labels: dict = None + ) -> dict: + """Builds a basic cluster with the given name and k8s_namespace parameters. + + Parameters: + - name (str): The name of the cluster. + - k8s_namespace (str, optional): The kubernetes namespace for the cluster, with a default value of "default". + + Returns: + dict: The basic cluster as a dictionary. + """ + cluster: dict = ( + self.cluster_builder.build_meta( + name=name, k8s_namespace=k8s_namespace, labels=labels + ) + .build_head() + .get_cluster() + ) + + if self.cluster_builder.succeeded: + return cluster + return None + + def build_small_cluster( + self, name: str, k8s_namespace: str = "default", labels: dict = None + ) -> dict: + """Builds a small cluster with the given name and k8s_namespace parameters with 1 workergroup, + the workgroup has 1 replica with 2 cpu and 2G memory limits + + Parameters: + - name (str): The name of the cluster. + - k8s_namespace (str, optional): The kubernetes namespace for the cluster, with a default value of "default". + + Returns: + dict: The small cluster as a dictionary. + """ + cluster: dict = ( + self.cluster_builder.build_meta( + name=name, k8s_namespace=k8s_namespace, labels=labels + ) + .build_head() + .build_worker( + group_name="{}-workers".format(name), + replicas=1, + min_replicas=0, + max_replicas=2, + cpu_requests="1", + memory_requests="1G", + cpu_limits="2", + memory_limits="2G", + ) + .get_cluster() + ) + + if self.cluster_builder.succeeded: + return cluster + return None + + def build_medium_cluster( + self, name: str, k8s_namespace: str = "default", labels: str = None + ) -> dict: + """Builds a medium cluster with the given name and k8s_namespace parameters with 1 workergroup, + the workgroup has 3 replicas with 4 cpu and 4G memory limits + + Parameters: + - name (str): The name of the cluster. + - k8s_namespace (str, optional): The kubernetes namespace for the cluster, with a default value of "default". + + Returns: + dict: The small cluster as a dictionary. + """ + cluster: dict = ( + self.cluster_builder.build_meta( + name=name, k8s_namespace=k8s_namespace, labels=labels + ) + .build_head() + .build_worker( + group_name="{}-workers".format(name), + replicas=3, + min_replicas=0, + max_replicas=6, + cpu_requests="2", + memory_requests="2G", + cpu_limits="4", + memory_limits="4G", + ) + .get_cluster() + ) + + if self.cluster_builder.succeeded: + return cluster + return None + + def build_large_cluster( + self, name: str, k8s_namespace: str = "default", labels: dict = None + ) -> dict: + """Builds a medium cluster with the given name and k8s_namespace parameters. with 1 workergroup, + the workgroup has 6 replicas with 6 cpu and 6G memory limits + + Parameters: + - name (str): The name of the cluster. + - k8s_namespace (str, optional): The kubernetes namespace for the cluster, with a default value of "default". + + Returns: + dict: The small cluster as a dictionary. + """ + cluster: dict = ( + self.cluster_builder.build_meta( + name=name, k8s_namespace=k8s_namespace, labels=labels + ) + .build_head() + .build_worker( + group_name="{}-workers".format(name), + replicas=6, + min_replicas=0, + max_replicas=12, + cpu_requests="3", + memory_requests="4G", + cpu_limits="6", + memory_limits="8G", + ) + .get_cluster() + ) + + if self.cluster_builder.succeeded: + return cluster + return None diff --git a/src/codeflare_sdk/vendored/python_client/utils/kuberay_cluster_utils.py b/src/codeflare_sdk/vendored/python_client/utils/kuberay_cluster_utils.py new file mode 100644 index 00000000..ac36fa93 --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client/utils/kuberay_cluster_utils.py @@ -0,0 +1,473 @@ +""" +Set of helper methods to manage rayclusters. Requires Python 3.6 and higher +""" + +import logging +import copy +import re +from typing import Any, Tuple +from codeflare_sdk.vendored.python_client import constants + + +log = logging.getLogger(__name__) +if logging.getLevelName(log.level) == "NOTSET": + logging.basicConfig(format="%(asctime)s %(message)s", level=constants.LOGLEVEL) + +""" +ClusterUtils contains methods to facilitate modifying/populating the config of a raycluster +""" + + +class ClusterUtils: + """ + ClusterUtils - Utility class for populating cluster information + + Methods: + - populate_meta(cluster: dict, name: str, k8s_namespace: str, labels: dict, ray_version: str) -> dict: + - populate_ray_head(cluster: dict, ray_image: str,service_type: str, cpu_requests: str, memory_requests: str, cpu_limits: str, memory_limits: str, ray_start_params: dict) -> Tuple[dict, bool]: + - populate_worker_group(cluster: dict, group_name: str, ray_image: str, ray_command: Any, init_image: str, cpu_requests: str, memory_requests: str, cpu_limits: str, memory_limits: str, replicas: int, min_replicas: int, max_replicas: int, ray_start_params: dict) -> Tuple[dict, bool]: + - update_worker_group_replicas(cluster: dict, group_name: str, max_replicas: int, min_replicas: int, replicas: int) -> Tuple[dict, bool]: + """ + + def populate_meta( + self, + cluster: dict, + name: str, + k8s_namespace: str, + labels: dict, + ray_version: str, + ) -> dict: + """Populate the metadata and ray version of the cluster. + + Parameters: + - cluster (dict): A dictionary representing a cluster. + - name (str): The name of the cluster. + - k8s_namespace (str): The namespace of the cluster. + - labels (dict): A dictionary of labels to be applied to the cluster. + - ray_version (str): The version of Ray to use in the cluster. + + Returns: + dict: The updated cluster dictionary with metadata and ray version populated. + """ + + assert self.is_valid_name(name) + + cluster["apiVersion"] = "{group}/{version}".format( + group=constants.GROUP, version=constants.CLUSTER_VERSION + ) + cluster["kind"] = constants.CLUSTER_KIND + cluster["metadata"] = { + "name": name, + "namespace": k8s_namespace, + "labels": labels, + } + cluster["spec"] = {"rayVersion": ray_version} + return cluster + + def populate_ray_head( + self, + cluster: dict, + ray_image: str, + service_type: str, + cpu_requests: str, + memory_requests: str, + cpu_limits: str, + memory_limits: str, + ray_start_params: dict, + ) -> Tuple[dict, bool]: + """Populate the ray head specs of the cluster + Parameters: + - cluster (dict): The dictionary representation of the cluster. + - ray_image (str): The name of the ray image to use for the head node. + - service_type (str): The type of service to run for the head node. + - cpu_requests (str): The CPU resource requests for the head node. + - memory_requests (str): The memory resource requests for the head node. + - cpu_limits (str): The CPU resource limits for the head node. + - memory_limits (str): The memory resource limits for the head node. + - ray_start_params (dict): The parameters for starting the Ray cluster. + + Returns: + - Tuple (dict, bool): The updated cluster, and a boolean indicating whether the update was successful. + """ + # validate arguments + try: + arguments = locals() + for k, v in arguments.items(): + assert v + except AssertionError as e: + log.error( + "error creating ray head, the parameters are not fully defined. {} = {}".format( + k, v + ) + ) + return cluster, False + + # make sure metadata exists + if "spec" in cluster.keys(): + if "headGroupSpec" not in cluster.keys(): + log.info( + "setting the headGroupSpec for cluster {}".format( + cluster["metadata"]["name"] + ) + ) + cluster["spec"]["headGroupSpec"] = [] + else: + log.error("error creating ray head, the spec and/or metadata is not define") + return cluster, False + + # populate headGroupSpec + cluster["spec"]["headGroupSpec"] = { + "serviceType": service_type, + "rayStartParams": ray_start_params, + "template": { + "spec": { + "containers": [ + { + "image": ray_image, + "name": "ray-head", + "ports": [ + { + "containerPort": 6379, + "name": "gcs-server", + "protocol": "TCP", + }, + { + "containerPort": 8265, + "name": "dashboard", + "protocol": "TCP", + }, + { + "containerPort": 10001, + "name": "client", + "protocol": "TCP", + }, + ], + "resources": { + "requests": { + "cpu": cpu_requests, + "memory": memory_requests, + }, + "limits": {"cpu": cpu_limits, "memory": memory_limits}, + }, + "volumeMounts": [ + {"mountPath": "/tmp/ray", "name": "ray-logs"} + ], + } + ], + "volumes": [{"emptyDir": {}, "name": "ray-logs"}], + } + }, + } + + return cluster, True + + def populate_worker_group( + self, + group_name: str, + ray_image: str, + ray_command: Any, + init_image: str, + cpu_requests: str, + memory_requests: str, + cpu_limits: str, + memory_limits: str, + replicas: int, + min_replicas: int, + max_replicas: int, + ray_start_params: dict, + ) -> Tuple[dict, bool]: + """Populate the worker group specification in the cluster dictionary. + + Parameters: + - cluster (dict): Dictionary representing the cluster spec. + - group_name (str): The name of the worker group. + - ray_image (str): The image to use for the Ray worker containers. + - ray_command (Any): The command to run in the Ray worker containers. + - init_image (str): The init container image to use. + - cpu_requests (str): The requested CPU resources for the worker containers. + - memory_requests (str): The requested memory resources for the worker containers. + - cpu_limits (str): The limit on CPU resources for the worker containers. + - memory_limits (str): The limit on memory resources for the worker containers. + - replicas (int): The desired number of replicas for the worker group. + - min_replicas (int): The minimum number of replicas for the worker group. + - max_replicas (int): The maximum number of replicas for the worker group. + - ray_start_params (dict): The parameters to pass to the Ray worker start command. + + Returns: + - Tuple[dict, bool]: A tuple of the cluster specification and a boolean indicating + whether the worker group was successfully populated. + """ + # validate arguments + try: + arguments = locals() + for k, v in arguments.items(): + if k != "min_replicas" and k != "ray_start_params": + assert v + except AssertionError as e: + log.error( + "error populating worker group, the parameters are not fully defined. {} = {}".format( + k, v + ) + ) + return None, False + + assert self.is_valid_name(group_name) + assert max_replicas >= min_replicas + + worker_group: dict[str, Any] = { + "groupName": group_name, + "maxReplicas": max_replicas, + "minReplicas": min_replicas, + "rayStartParams": ray_start_params, + "replicas": replicas, + "template": { + "spec": { + "containers": [ + { + "image": ray_image, + "command": ray_command, + "lifecycle": { + "preStop": { + "exec": {"command": ["/bin/sh", "-c", "ray stop"]} + } + }, + "name": "ray-worker", + "resources": { + "requests": { + "cpu": cpu_requests, + "memory": memory_requests, + }, + "limits": { + "cpu": cpu_limits, + "memory": memory_limits, + }, + }, + "volumeMounts": [ + {"mountPath": "/tmp/ray", "name": "ray-logs"} + ], + } + ], + "volumes": [{"emptyDir": {}, "name": "ray-logs"}], + } + }, + } + + return worker_group, True + + def update_worker_group_replicas( + self, + cluster: dict, + group_name: str, + max_replicas: int, + min_replicas: int, + replicas: int, + ) -> Tuple[dict, bool]: + """Update the number of replicas for a worker group in the cluster. + + Parameters: + - cluster (dict): The cluster to update. + - group_name (str): The name of the worker group to update. + - max_replicas (int): The maximum number of replicas for the worker group. + - min_replicas (int): The minimum number of replicas for the worker group. + - replicas (int): The desired number of replicas for the worker group. + + Returns: + Tuple[dict, bool]: A tuple containing the updated cluster and a flag indicating whether the update was successful. + """ + try: + arguments = locals() + for k, v in arguments.items(): + if k != "min_replicas": + assert v + except AssertionError as e: + log.error( + "error updating worker group, the parameters are not fully defined. {} = {}".format( + k, v + ) + ) + return cluster, False + + assert cluster["spec"]["workerGroupSpecs"] + assert max_replicas >= min_replicas + + for i in range(len(cluster["spec"]["workerGroupSpecs"])): + if cluster["spec"]["workerGroupSpecs"][i]["groupName"] == group_name: + cluster["spec"]["workerGroupSpecs"][i]["maxReplicas"] = max_replicas + cluster["spec"]["workerGroupSpecs"][i]["minReplicas"] = min_replicas + cluster["spec"]["workerGroupSpecs"][i]["replicas"] = replicas + return cluster, True + + return cluster, False + + def update_worker_group_resources( + self, + cluster: dict, + group_name: str, + cpu_requests: str, + memory_requests: str, + cpu_limits: str, + memory_limits: str, + container_name="unspecified", + ) -> Tuple[dict, bool]: + """Update the resources for a worker group pods in the cluster. + + Parameters: + - cluster (dict): The cluster to update. + - group_name (str): The name of the worker group to update. + - cpu_requests (str): CPU requests for the worker pods. + - memory_requests (str): Memory requests for the worker pods. + - cpu_limits (str): CPU limits for the worker pods. + - memory_limits (str): Memory limits for the worker pods. + + Returns: + Tuple[dict, bool]: A tuple containing the updated cluster and a flag indicating whether the update was successful. + """ + try: + arguments = locals() + for k, v in arguments.items(): + if k != "min_replicas": + assert v + except AssertionError as e: + log.error( + "error updating worker group, the parameters are not fully defined. {} = {}".format( + k, v + ) + ) + return cluster, False + + assert cluster["spec"]["workerGroupSpecs"] + + worker_groups = cluster["spec"]["workerGroupSpecs"] + + def add_values(group_index: int, container_index: int): + worker_groups[group_index]["template"]["spec"]["containers"][ + container_index + ]["resources"]["requests"]["cpu"] = cpu_requests + worker_groups[group_index]["template"]["spec"]["containers"][ + container_index + ]["resources"]["requests"]["memory"] = memory_requests + worker_groups[group_index]["template"]["spec"]["containers"][ + container_index + ]["resources"]["limits"]["cpu"] = cpu_limits + worker_groups[group_index]["template"]["spec"]["containers"][ + container_index + ]["resources"]["limits"]["memory"] = memory_limits + + for group_index, worker_group in enumerate(worker_groups): + if worker_group["groupName"] != group_name: + continue + + containers = worker_group["template"]["spec"]["containers"] + container_names = [container["name"] for container in containers] + + if len(containers) == 0: + log.error( + f"error updating container resources, the worker group {group_name} has no containers" + ) + return cluster, False + + if container_name == "unspecified": + add_values(group_index, 0) + return cluster, True + elif container_name == "all_containers": + for container_index in range(len(containers)): + add_values(group_index, container_index) + return cluster, True + elif container_name in container_names: + container_index = container_names.index(container_name) + add_values(group_index, container_index) + return cluster, True + + return cluster, False + + def duplicate_worker_group( + self, + cluster: dict, + group_name: str, + new_group_name: str, + ) -> Tuple[dict, bool]: + """Duplicate a worker group in the cluster. + + Parameters: + - cluster (dict): The cluster definition. + - group_name (str): The name of the worker group to be duplicated. + - new_group_name (str): The name for the duplicated worker group. + + Returns: + Tuple[dict, bool]: A tuple containing the updated cluster definition and a boolean indicating the success of the operation. + """ + try: + arguments = locals() + for k, v in arguments.items(): + assert v + except AssertionError as e: + log.error( + f"error duplicating worker group, the parameters are not fully defined. {k} = {v}" + ) + return cluster, False + assert self.is_valid_name(new_group_name) + assert cluster["spec"]["workerGroupSpecs"] + + worker_groups = cluster["spec"]["workerGroupSpecs"] + for _, worker_group in enumerate(worker_groups): + if worker_group["groupName"] == group_name: + duplicate_group = copy.deepcopy(worker_group) + duplicate_group["groupName"] = new_group_name + worker_groups.append(duplicate_group) + return cluster, True + + log.error( + f"error duplicating worker group, no match was found for {group_name}" + ) + return cluster, False + + def delete_worker_group( + self, + cluster: dict, + group_name: str, + ) -> Tuple[dict, bool]: + """Deletes a worker group in the cluster. + + Parameters: + - cluster (dict): The cluster definition. + - group_name (str): The name of the worker group to be duplicated. + + Returns: + Tuple[dict, bool]: A tuple containing the updated cluster definition and a boolean indicating the success of the operation. + """ + try: + arguments = locals() + for k, v in arguments.items(): + assert v + except AssertionError as e: + log.error( + f"error creating ray head, the parameters are not fully defined. {k} = {v}" + ) + return cluster, False + + assert cluster["spec"]["workerGroupSpecs"] + + worker_groups = cluster["spec"]["workerGroupSpecs"] + first_or_none = next( + (x for x in worker_groups if x["groupName"] == group_name), None + ) + if first_or_none: + worker_groups.remove(first_or_none) + return cluster, True + + log.error(f"error removing worker group, no match was found for {group_name}") + return cluster, False + + def is_valid_name(self, name: str) -> bool: + msg = "The name must be 63 characters or less, begin and end with an alphanumeric character, and contain only dashes, dots, and alphanumerics." + if len(name) > 63 or not bool(re.match("^[a-z0-9]([-.]*[a-z0-9])+$", name)): + log.error(msg) + return False + return True + + def is_valid_label(self, name: str) -> bool: + msg = "The label name must be 63 characters or less, begin and end with an alphanumeric character, and contain only dashes, underscores, dots, and alphanumerics." + if len(name) > 63 or not bool(re.match("^[a-z0-9]([-._]*[a-z0-9])+$", name)): + log.error(msg) + return False + return True diff --git a/src/codeflare_sdk/vendored/python_client_test/README.md b/src/codeflare_sdk/vendored/python_client_test/README.md new file mode 100644 index 00000000..6c32e260 --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client_test/README.md @@ -0,0 +1,29 @@ +# Overview + +## For developers + +1. `pip install -U pip setuptools` +1. `cd clients/python-client && pip install -e .` + +Uninstall with `pip uninstall python-client`. + +## For testing run + +`python -m unittest discover 'clients/python-client/python_client_test/'` + +### Coverage report + +#### Pre-requisites + +* `sudo apt install libsqlite3-dev` +* `pyenv install 3.6.5` # or your Python version +* `pip install db-sqlite3 coverage` + +__To gather data__ +`python -m coverage run -m unittest` + +__to generate a coverage report__ +`python -m coverage report` + +__to generate the test coverage report in HTML format__ +`python -m coverage html` diff --git a/src/codeflare_sdk/vendored/python_client_test/helpers.py b/src/codeflare_sdk/vendored/python_client_test/helpers.py new file mode 100644 index 00000000..1bcfdbc2 --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client_test/helpers.py @@ -0,0 +1,135 @@ +import time +from codeflare_sdk.vendored.python_client import constants + + +def create_job_with_cluster_selector( + job_name, + namespace, + cluster_name, + entrypoint="python -c \"import ray; ray.init(); @ray.remote\ndef hello(): return 'Hello from Ray!'; print(ray.get(hello.remote()))\"", + labels=None, +): + job_body = { + "apiVersion": constants.GROUP + "/" + constants.JOB_VERSION, + "kind": constants.JOB_KIND, + "metadata": { + "name": job_name, + "namespace": namespace, + "labels": { + "app.kubernetes.io/name": job_name, + "app.kubernetes.io/managed-by": "kuberay", + }, + }, + "spec": { + "clusterSelector": { + "ray.io/cluster": cluster_name, + }, + "entrypoint": entrypoint, + "submissionMode": "K8sJobMode", + }, + } + + # Add any additional labels if provided + if labels: + job_body["metadata"]["labels"].update(labels) + + return job_body + + +def create_job_with_ray_cluster_spec( + job_name, + namespace, + entrypoint="python -c \"import ray; ray.init(); @ray.remote\ndef hello(): return 'Hello from Ray!'; print(ray.get(hello.remote()))\"", + labels=None, +): + job_body = { + "apiVersion": constants.GROUP + "/" + constants.JOB_VERSION, + "kind": constants.JOB_KIND, + "metadata": { + "name": job_name, + "namespace": namespace, + "labels": { + "app.kubernetes.io/name": job_name, + "app.kubernetes.io/managed-by": "kuberay", + }, + }, + "spec": { + "rayClusterSpec": { + "headGroupSpec": { + "serviceType": "ClusterIP", + "replicas": 1, + "rayStartParams": { + "dashboard-host": "0.0.0.0", + }, + "template": { + "spec": { + "containers": [ + { + "name": "ray-head", + "image": "rayproject/ray:2.48.0", + "ports": [ + {"containerPort": 6379, "name": "gcs"}, + { + "containerPort": 8265, + "name": "dashboard", + }, + { + "containerPort": 10001, + "name": "client", + }, + ], + "resources": { + "limits": { + "cpu": "1", + "memory": "2Gi", + }, + "requests": { + "cpu": "500m", + "memory": "1Gi", + }, + }, + } + ] + } + }, + }, + "workerGroupSpecs": [ + { + "groupName": "small-worker", + "replicas": 1, + "rayStartParams": { + "num-cpus": "1", + }, + "template": { + "spec": { + "containers": [ + { + "name": "ray-worker", + "image": "rayproject/ray:2.48.0", + "resources": { + "limits": { + "cpu": "1", + "memory": "1Gi", + }, + "requests": { + "cpu": "500m", + "memory": "512Mi", + }, + }, + } + ] + } + }, + } + ], + }, + "entrypoint": entrypoint, + "submissionMode": "K8sJobMode", + "shutdownAfterJobFinishes": True, + }, + } + + if labels: + job_body["metadata"]["labels"].update(labels) + + return job_body diff --git a/src/codeflare_sdk/vendored/python_client_test/test_cluster_api.py b/src/codeflare_sdk/vendored/python_client_test/test_cluster_api.py new file mode 100644 index 00000000..3fdb18e7 --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client_test/test_cluster_api.py @@ -0,0 +1,345 @@ +import unittest +from codeflare_sdk.vendored.python_client import kuberay_cluster_api, constants +from codeflare_sdk.vendored.python_client.utils import kuberay_cluster_builder + + +# Keep the original test cluster body for reference if needed +test_cluster_body: dict = { + "apiVersion": "ray.io/v1", + "kind": "RayCluster", + "metadata": { + "labels": {"controller-tools.k8s.io": "1.0"}, + "name": "raycluster-complete-raw", + }, + "spec": { + "rayVersion": "2.46.0", + "headGroupSpec": { + "rayStartParams": {"dashboard-host": "0.0.0.0"}, + "template": { + "metadata": {"labels": {}}, + "spec": { + "containers": [ + { + "name": "ray-head", + "image": "rayproject/ray:2.46.0", + "ports": [ + {"containerPort": 6379, "name": "gcs"}, + {"containerPort": 8265, "name": "dashboard"}, + {"containerPort": 10001, "name": "client"}, + ], + "lifecycle": { + "preStop": { + "exec": {"command": ["/bin/sh", "-c", "ray stop"]} + } + }, + "volumeMounts": [ + {"mountPath": "/tmp/ray", "name": "ray-logs"} + ], + "resources": { + "limits": {"cpu": "1", "memory": "2G"}, + "requests": {"cpu": "500m", "memory": "2G"}, + }, + } + ], + "volumes": [{"name": "ray-logs", "emptyDir": {}}], + }, + }, + }, + "workerGroupSpecs": [ + { + "replicas": 1, + "minReplicas": 1, + "maxReplicas": 10, + "groupName": "small-group", + "rayStartParams": {}, + "template": { + "spec": { + "containers": [ + { + "name": "ray-worker", + "image": "rayproject/ray:2.46.0", + "lifecycle": { + "preStop": { + "exec": { + "command": ["/bin/sh", "-c", "ray stop"] + } + } + }, + "volumeMounts": [ + {"mountPath": "/tmp/ray", "name": "ray-logs"} + ], + "resources": { + "limits": {"cpu": "1", "memory": "1G"}, + "requests": {"cpu": "500m", "memory": "1G"}, + }, + }, + { + "name": "side-car", + "image": "rayproject/ray:2.46.0", + "resources": { + "limits": {"cpu": "1", "memory": "1G"}, + "requests": {"cpu": "500m", "memory": "1G"}, + }, + }, + ], + "volumes": [{"name": "ray-logs", "emptyDir": {}}], + } + }, + } + ], + }, + "status": { + "state": "ready", + "availableWorkerReplicas": 2, + "desiredWorkerReplicas": 1, + "endpoints": {"client": "10001", "dashboard": "8265", "gcs-server": "6379"}, + "head": {"serviceIP": "10.152.183.194"}, + "lastUpdateTime": "2023-02-16T05:15:17Z", + "maxWorkerReplicas": 2, + }, +} + + +class TestClusterApi(unittest.TestCase): + """Comprehensive test suite for RayClusterApi functionality.""" + + def __init__(self, methodName: str = ...) -> None: + super().__init__(methodName) + self.api = kuberay_cluster_api.RayClusterApi() + self.director = kuberay_cluster_builder.Director() + + def test_create_and_get_ray_cluster(self): + """Test creating a cluster and retrieving it.""" + cluster_name = "test-create-cluster" + namespace = "default" + + # Build a small cluster using the director + cluster_body = self.director.build_small_cluster( + name=cluster_name, + k8s_namespace=namespace, + labels={"test": "create-cluster"}, + ) + + # Ensure cluster was built successfully + self.assertIsNotNone(cluster_body, "Cluster should be built successfully") + self.assertEqual(cluster_body["metadata"]["name"], cluster_name) + + try: + # Create the cluster + created_cluster = self.api.create_ray_cluster( + body=cluster_body, k8s_namespace=namespace + ) + self.assertIsNotNone( + created_cluster, "Cluster should be created successfully" + ) + self.assertEqual(created_cluster["metadata"]["name"], cluster_name) + + # Get the cluster and verify it exists + retrieved_cluster = self.api.get_ray_cluster( + name=cluster_name, k8s_namespace=namespace + ) + self.assertIsNotNone( + retrieved_cluster, "Cluster should be retrieved successfully" + ) + self.assertEqual(retrieved_cluster["metadata"]["name"], cluster_name) + self.assertEqual( + retrieved_cluster["spec"]["rayVersion"], + cluster_body["spec"]["rayVersion"], + ) + + finally: + # Clean up + self.api.delete_ray_cluster(name=cluster_name, k8s_namespace=namespace) + + def test_list_ray_clusters(self): + """Test listing Ray clusters in a namespace.""" + cluster_name_1 = "test-list-cluster-1" + cluster_name_2 = "test-list-cluster-2" + namespace = "default" + test_label = "test-list-clusters" + + # Build two small clusters + cluster_body_1 = self.director.build_small_cluster( + name=cluster_name_1, + k8s_namespace=namespace, + labels={"test": test_label}, + ) + cluster_body_2 = self.director.build_small_cluster( + name=cluster_name_2, + k8s_namespace=namespace, + labels={"test": test_label}, + ) + + try: + # Create both clusters + created_cluster_1 = self.api.create_ray_cluster( + body=cluster_body_1, k8s_namespace=namespace + ) + created_cluster_2 = self.api.create_ray_cluster( + body=cluster_body_2, k8s_namespace=namespace + ) + + self.assertIsNotNone(created_cluster_1, "First cluster should be created") + self.assertIsNotNone(created_cluster_2, "Second cluster should be created") + + # List all clusters + clusters_list = self.api.list_ray_clusters(k8s_namespace=namespace) + self.assertIsNotNone(clusters_list, "Should be able to list clusters") + self.assertIn("items", clusters_list, "Response should contain items") + + # Verify our test clusters are in the list + cluster_names = [ + item["metadata"]["name"] for item in clusters_list["items"] + ] + self.assertIn( + cluster_name_1, + cluster_names, + "First test cluster should be in the list", + ) + self.assertIn( + cluster_name_2, + cluster_names, + "Second test cluster should be in the list", + ) + + # Test listing with label selector + labeled_clusters = self.api.list_ray_clusters( + k8s_namespace=namespace, label_selector=f"test={test_label}" + ) + self.assertIsNotNone( + labeled_clusters, "Should be able to list clusters with label selector" + ) + labeled_cluster_names = [ + item["metadata"]["name"] for item in labeled_clusters["items"] + ] + self.assertIn( + cluster_name_1, + labeled_cluster_names, + "First test cluster should match label", + ) + self.assertIn( + cluster_name_2, + labeled_cluster_names, + "Second test cluster should match label", + ) + + finally: + # Clean up both clusters + self.api.delete_ray_cluster(name=cluster_name_1, k8s_namespace=namespace) + self.api.delete_ray_cluster(name=cluster_name_2, k8s_namespace=namespace) + + def test_cluster_status_and_wait_until_running(self): + """Test getting cluster status and waiting for cluster to be ready.""" + cluster_name = "test-status-cluster" + namespace = "default" + + # Build a small cluster + cluster_body = self.director.build_small_cluster( + name=cluster_name, + k8s_namespace=namespace, + labels={"test": "status-cluster"}, + ) + + try: + # Create the cluster + created_cluster = self.api.create_ray_cluster( + body=cluster_body, k8s_namespace=namespace + ) + self.assertIsNotNone( + created_cluster, "Cluster should be created successfully" + ) + + # Test getting cluster status (may take some time to populate) + status = self.api.get_ray_cluster_status( + name=cluster_name, + k8s_namespace=namespace, + timeout=120, + delay_between_attempts=5, + ) + self.assertIsNotNone(status, "Cluster status should be retrieved") + + # Test waiting for cluster to be running + is_running = self.api.wait_until_ray_cluster_running( + name=cluster_name, + k8s_namespace=namespace, + timeout=180, + delay_between_attempts=10, + ) + self.assertTrue(is_running, "Cluster should become ready within timeout") + + # Verify final status after cluster is ready + final_status = self.api.get_ray_cluster_status( + name=cluster_name, + k8s_namespace=namespace, + timeout=10, + delay_between_attempts=2, + ) + self.assertIsNotNone(final_status, "Final status should be available") + self.assertIn("state", final_status, "Status should contain state field") + self.assertEqual( + final_status["state"], "ready", "Cluster should be in ready state" + ) + + finally: + # Clean up + self.api.delete_ray_cluster(name=cluster_name, k8s_namespace=namespace) + + def test_patch_ray_cluster(self): + """Test patching an existing Ray cluster.""" + cluster_name = "test-patch-cluster" + namespace = "default" + + # Build a small cluster + cluster_body = self.director.build_small_cluster( + name=cluster_name, + k8s_namespace=namespace, + labels={"test": "patch-cluster"}, + ) + + try: + # Create the cluster + created_cluster = self.api.create_ray_cluster( + body=cluster_body, k8s_namespace=namespace + ) + self.assertIsNotNone( + created_cluster, "Cluster should be created successfully" + ) + + # Wait for cluster to be ready before patching + self.api.wait_until_ray_cluster_running( + name=cluster_name, + k8s_namespace=namespace, + timeout=180, + delay_between_attempts=10, + ) + + # Create a patch to update the cluster (e.g., add a label) + patch_data = { + "metadata": {"labels": {"test": "patch-cluster", "patched": "true"}} + } + + # Apply the patch + patch_result = self.api.patch_ray_cluster( + name=cluster_name, ray_patch=patch_data, k8s_namespace=namespace + ) + self.assertTrue(patch_result, "Patch operation should succeed") + + # Verify the patch was applied + updated_cluster = self.api.get_ray_cluster( + name=cluster_name, k8s_namespace=namespace + ) + self.assertIsNotNone(updated_cluster, "Updated cluster should be retrieved") + self.assertIn( + "patched", + updated_cluster["metadata"]["labels"], + "Patched label should be present", + ) + self.assertEqual( + updated_cluster["metadata"]["labels"]["patched"], + "true", + "Patched label should have correct value", + ) + + finally: + # Clean up + self.api.delete_ray_cluster(name=cluster_name, k8s_namespace=namespace) diff --git a/src/codeflare_sdk/vendored/python_client_test/test_director.py b/src/codeflare_sdk/vendored/python_client_test/test_director.py new file mode 100644 index 00000000..07536971 --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client_test/test_director.py @@ -0,0 +1,121 @@ +import unittest +from codeflare_sdk.vendored.python_client.utils import kuberay_cluster_builder + + +class TestDirector(unittest.TestCase): + def __init__(self, methodName: str = ...) -> None: + super().__init__(methodName) + self.director = kuberay_cluster_builder.Director() + + def test_build_basic_cluster(self): + cluster = self.director.build_basic_cluster(name="basic-cluster") + # testing meta + actual = cluster["metadata"]["name"] + expected = "basic-cluster" + self.assertEqual(actual, expected) + + actual = cluster["metadata"]["namespace"] + expected = "default" + self.assertEqual(actual, expected) + + # testing the head pod + actual = cluster["spec"]["headGroupSpec"]["template"]["spec"]["containers"][0][ + "resources" + ]["requests"]["cpu"] + expected = "2" + self.assertEqual(actual, expected) + + def test_build_small_cluster(self): + cluster = self.director.build_small_cluster(name="small-cluster") + # testing meta + actual = cluster["metadata"]["name"] + expected = "small-cluster" + self.assertEqual(actual, expected) + + actual = cluster["metadata"]["namespace"] + expected = "default" + self.assertEqual(actual, expected) + + # testing the head pod + actual = cluster["spec"]["headGroupSpec"]["template"]["spec"]["containers"][0][ + "resources" + ]["requests"]["cpu"] + expected = "2" + self.assertEqual(actual, expected) + + # testing the workergroup + actual = cluster["spec"]["workerGroupSpecs"][0]["replicas"] + expected = 1 + self.assertEqual(actual, expected) + + actual = cluster["spec"]["workerGroupSpecs"][0]["template"]["spec"][ + "containers" + ][0]["resources"]["requests"]["cpu"] + expected = "1" + self.assertEqual(actual, expected) + + def test_build_medium_cluster(self): + cluster = self.director.build_medium_cluster(name="medium-cluster") + # testing meta + actual = cluster["metadata"]["name"] + expected = "medium-cluster" + self.assertEqual(actual, expected) + + actual = cluster["metadata"]["namespace"] + expected = "default" + self.assertEqual(actual, expected) + + # testing the head pod + actual = cluster["spec"]["headGroupSpec"]["template"]["spec"]["containers"][0][ + "resources" + ]["requests"]["cpu"] + expected = "2" + self.assertEqual(actual, expected) + + # testing the workergroup + actual = cluster["spec"]["workerGroupSpecs"][0]["replicas"] + expected = 3 + self.assertEqual(actual, expected) + + actual = cluster["spec"]["workerGroupSpecs"][0]["groupName"] + expected = "medium-cluster-workers" + self.assertEqual(actual, expected) + + actual = cluster["spec"]["workerGroupSpecs"][0]["template"]["spec"][ + "containers" + ][0]["resources"]["requests"]["cpu"] + expected = "2" + self.assertEqual(actual, expected) + + def test_build_large_cluster(self): + cluster = self.director.build_large_cluster(name="large-cluster") + # testing meta + actual = cluster["metadata"]["name"] + expected = "large-cluster" + self.assertEqual(actual, expected) + + actual = cluster["metadata"]["namespace"] + expected = "default" + self.assertEqual(actual, expected) + + # testing the head pod + actual = cluster["spec"]["headGroupSpec"]["template"]["spec"]["containers"][0][ + "resources" + ]["requests"]["cpu"] + expected = "2" + self.assertEqual(actual, expected) + + # testing the workergroup + actual = cluster["spec"]["workerGroupSpecs"][0]["replicas"] + expected = 6 + self.assertEqual(actual, expected) + + actual = cluster["spec"]["workerGroupSpecs"][0]["groupName"] + expected = "large-cluster-workers" + self.assertEqual(actual, expected) + + actual = cluster["spec"]["workerGroupSpecs"][0]["template"]["spec"][ + "containers" + ][0]["resources"]["requests"]["cpu"] + expected = "3" + self.assertEqual(actual, expected) diff --git a/src/codeflare_sdk/vendored/python_client_test/test_job_api.py b/src/codeflare_sdk/vendored/python_client_test/test_job_api.py new file mode 100644 index 00000000..bad75edc --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client_test/test_job_api.py @@ -0,0 +1,567 @@ +import time +import unittest +from codeflare_sdk.vendored.python_client import kuberay_job_api, kuberay_cluster_api +from codeflare_sdk.vendored.python_client.utils import kuberay_cluster_builder +from helpers import create_job_with_cluster_selector, create_job_with_ray_cluster_spec + +namespace = "default" + + +class TestJobApi(unittest.TestCase): + def __init__(self, methodName: str = ...) -> None: + super().__init__(methodName) + + self.api = kuberay_job_api.RayjobApi() + self.cluster_api = kuberay_cluster_api.RayClusterApi() + self.director = kuberay_cluster_builder.Director() + + def test_submit_ray_job_to_existing_cluster(self): + """Test submitting a job to an existing cluster using clusterSelector.""" + cluster_name = "premade" + + cluster_body = self.director.build_small_cluster( + name=cluster_name, + k8s_namespace=namespace, + labels={"ray.io/cluster": cluster_name}, + ) + + self.assertIsNotNone(cluster_body, "Cluster should be built successfully") + self.assertEqual(cluster_body["metadata"]["name"], cluster_name) + + created_cluster = self.cluster_api.create_ray_cluster( + body=cluster_body, k8s_namespace=namespace + ) + + self.assertIsNotNone(created_cluster, "Cluster should be created successfully") + + self.cluster_api.wait_until_ray_cluster_running(cluster_name, namespace, 60, 10) + job_name = "premade-cluster-job" + try: + job_body = create_job_with_cluster_selector( + job_name, + namespace, + cluster_name, + ) + submitted_job = self.api.submit_job( + job=job_body, + k8s_namespace=namespace, + ) + + self.assertIsNotNone(submitted_job, "Job should be submitted successfully") + self.assertEqual(submitted_job["metadata"]["name"], job_name) + self.assertEqual( + submitted_job["spec"]["clusterSelector"]["ray.io/cluster"], cluster_name + ) + + self.api.wait_until_job_finished(job_name, namespace, 120, 10) + finally: + self.cluster_api.delete_ray_cluster( + name=cluster_name, k8s_namespace=namespace + ) + + self.api.delete_job(job_name, namespace) + + def test_get_job_status(self): + """Test getting job status for a running job.""" + cluster_name = "status-test-cluster" + + cluster_body = self.director.build_small_cluster( + name=cluster_name, + k8s_namespace=namespace, + labels={"ray.io/cluster": cluster_name}, + ) + + created_cluster = self.cluster_api.create_ray_cluster( + body=cluster_body, k8s_namespace=namespace + ) + self.assertIsNotNone(created_cluster, "Cluster should be created successfully") + + self.cluster_api.wait_until_ray_cluster_running(cluster_name, namespace, 60, 10) + + job_name = "status-test-job" + try: + job_body = create_job_with_cluster_selector( + job_name, + namespace, + cluster_name, + ) + + submitted_job = self.api.submit_job( + job=job_body, + k8s_namespace=namespace, + ) + self.assertIsNotNone(submitted_job, "Job should be submitted successfully") + + status = self.api.get_job_status( + job_name, namespace, timeout=30, delay_between_attempts=2 + ) + self.assertIsNotNone(status, "Job status should be retrieved") + + # Verify expected status fields + self.assertIn( + "jobDeploymentStatus", + status, + "Status should contain jobDeploymentStatus field", + ) + self.assertIn("jobId", status, "Status should contain jobId field") + self.assertIn( + "rayClusterName", status, "Status should contain rayClusterName field" + ) + + self.api.wait_until_job_finished(job_name, namespace, 60, 5) + + final_status = self.api.get_job_status( + job_name, namespace, timeout=10, delay_between_attempts=1 + ) + self.assertIsNotNone(final_status, "Final job status should be retrieved") + + self.assertIn( + "jobDeploymentStatus", + final_status, + "Final status should contain jobDeploymentStatus field", + ) + + finally: + self.cluster_api.delete_ray_cluster( + name=cluster_name, k8s_namespace=namespace + ) + self.api.delete_job(job_name, namespace) + + def test_wait_until_job_finished(self): + """Test waiting for job completion.""" + cluster_name = "wait-test-cluster" + + cluster_body = self.director.build_small_cluster( + name=cluster_name, + k8s_namespace=namespace, + labels={"ray.io/cluster": cluster_name}, + ) + + created_cluster = self.cluster_api.create_ray_cluster( + body=cluster_body, k8s_namespace=namespace + ) + self.assertIsNotNone(created_cluster, "Cluster should be created successfully") + + self.cluster_api.wait_until_ray_cluster_running( + cluster_name, namespace, 180, 10 + ) + + job_name = "wait-test-job" + try: + job_body = create_job_with_cluster_selector( + job_name, + namespace, + cluster_name, + ) + + submitted_job = self.api.submit_job( + job=job_body, + k8s_namespace=namespace, + ) + self.assertIsNotNone(submitted_job, "Job should be submitted successfully") + + result = self.api.wait_until_job_finished( + job_name, namespace, timeout=180, delay_between_attempts=2 + ) + self.assertTrue(result, "Job should complete successfully within timeout") + + finally: + self.cluster_api.delete_ray_cluster( + name=cluster_name, k8s_namespace=namespace + ) + self.api.delete_job(job_name, namespace) + + def test_delete_job(self): + """Test deleting a job.""" + cluster_name = "delete-test-cluster" + + cluster_body = self.director.build_small_cluster( + name=cluster_name, + k8s_namespace=namespace, + labels={"ray.io/cluster": cluster_name}, + ) + + created_cluster = self.cluster_api.create_ray_cluster( + body=cluster_body, k8s_namespace=namespace + ) + self.assertIsNotNone(created_cluster, "Cluster should be created successfully") + + self.cluster_api.wait_until_ray_cluster_running(cluster_name, namespace, 60, 10) + + job_name = "delete-test-job" + try: + job_body = create_job_with_cluster_selector( + job_name, + namespace, + cluster_name, + ) + + submitted_job = self.api.submit_job( + job=job_body, + k8s_namespace=namespace, + ) + self.assertIsNotNone(submitted_job, "Job should be submitted successfully") + + self.api.wait_until_job_finished(job_name, namespace, 60, 5) + + delete_result = self.api.delete_job(job_name, namespace) + self.assertTrue(delete_result, "Job should be deleted successfully") + + finally: + self.cluster_api.delete_ray_cluster( + name=cluster_name, k8s_namespace=namespace + ) + + def test_get_job_status_nonexistent_job(self): + """Test getting status for a non-existent job.""" + status = self.api.get_job_status( + "nonexistent-job", namespace, timeout=2, delay_between_attempts=2 + ) + self.assertIsNone(status, "Status should be None for non-existent job") + + def test_wait_until_job_finished_nonexistent_job(self): + """Test waiting for completion of a non-existent job.""" + result = self.api.wait_until_job_finished( + "nonexistent-job", namespace, timeout=2, delay_between_attempts=2 + ) + self.assertFalse(result, "Should return False for non-existent job") + + def test_delete_job_nonexistent_job(self): + """Test deleting a non-existent job.""" + result = self.api.delete_job("nonexistent-job", namespace) + self.assertFalse(result, "Should return False for non-existent job") + + def test_submit_job_invalid_spec(self): + """Test submitting a job with invalid specification.""" + invalid_job = { + "apiVersion": "invalid/version", + "kind": "InvalidKind", + "metadata": { + "name": "invalid-job", + "namespace": namespace, + }, + "spec": { + "invalidField": "invalidValue", + }, + } + + result = self.api.submit_job(job=invalid_job, k8s_namespace=namespace) + self.assertIsNone(result, "Should return None for invalid job specification") + + def test_submit_job_with_ray_cluster_spec(self): + """Test submitting a job with rayClusterSpec - KubeRay will create and manage the cluster lifecycle.""" + job_name = "cluster-spec-job" + + try: + job_body = create_job_with_ray_cluster_spec( + job_name=job_name, + namespace=namespace, + ) + + submitted_job = self.api.submit_job( + job=job_body, + k8s_namespace=namespace, + ) + + self.assertIsNotNone(submitted_job, "Job should be submitted successfully") + self.assertEqual(submitted_job["metadata"]["name"], job_name) + + # Verify rayClusterSpec structure + self.assertIn( + "rayClusterSpec", + submitted_job["spec"], + "Job should have rayClusterSpec", + ) + self.assertIn( + "headGroupSpec", + submitted_job["spec"]["rayClusterSpec"], + "rayClusterSpec should have headGroupSpec", + ) + self.assertIn( + "workerGroupSpecs", + submitted_job["spec"]["rayClusterSpec"], + "rayClusterSpec should have workerGroupSpecs", + ) + + result = self.api.wait_until_job_finished(job_name, namespace, 300, 10) + self.assertTrue(result, "Job should complete successfully within timeout") + + final_status = self.api.get_job_status( + job_name, namespace, timeout=10, delay_between_attempts=1 + ) + self.assertIsNotNone(final_status, "Final job status should be retrieved") + self.assertIn( + "jobDeploymentStatus", + final_status, + "Final status should contain jobDeploymentStatus field", + ) + + finally: + self.api.delete_job(job_name, namespace) + + def test_suspend_job(self): + """Test stopping a running job.""" + job_name = "stop-test-job" + + try: + job_body = create_job_with_ray_cluster_spec( + job_name=job_name, + namespace=namespace, + ) + + submitted_job = self.api.submit_job( + job=job_body, + k8s_namespace=namespace, + ) + self.assertIsNotNone(submitted_job, "Job should be submitted successfully") + + result = self.api.wait_until_job_running( + job_name, namespace, timeout=120, delay_between_attempts=5 + ) + self.assertTrue(result, "Job should reach running state before suspension") + + stop_result = self.api.suspend_job(job_name, namespace) + self.assertTrue(stop_result, "Job should be suspended successfully") + + suspended = self.wait_for_job_status( + job_name, namespace, "Suspended", timeout=30 + ) + self.assertTrue(suspended, "Job deployment status should be Suspended") + + finally: + self.api.delete_job(job_name, namespace) + + def test_resubmit_job(self): + """Test resubmitting a suspended job.""" + job_name = "resubmit-test-job" + + try: + job_body = create_job_with_ray_cluster_spec( + job_name=job_name, + namespace=namespace, + ) + + submitted_job = self.api.submit_job( + job=job_body, + k8s_namespace=namespace, + ) + self.assertIsNotNone(submitted_job, "Job should be submitted successfully") + + result = self.api.wait_until_job_running( + job_name, namespace, timeout=120, delay_between_attempts=5 + ) + self.assertTrue(result, "Job should reach running state before suspension") + + stop_result = self.api.suspend_job(job_name, namespace) + self.assertTrue(stop_result, "Job should be suspended successfully") + + suspended = self.wait_for_job_status( + job_name, namespace, "Suspended", timeout=30 + ) + self.assertTrue( + suspended, "Job should be in Suspended status before resubmission" + ) + + resubmit_result = self.api.resubmit_job(job_name, namespace) + self.assertTrue(resubmit_result, "Job should be resubmitted successfully") + + result = self.api.wait_until_job_finished( + job_name, namespace, timeout=120, delay_between_attempts=5 + ) + self.assertTrue(result, "Resubmitted job should complete successfully") + + finally: + self.api.delete_job(job_name, namespace) + + def test_stop_and_resubmit_job(self): + """Test the full stop and resubmit cycle.""" + job_name = "stop-resubmit-job" + + try: + job_body = create_job_with_ray_cluster_spec( + job_name=job_name, + namespace=namespace, + ) + + submitted_job = self.api.submit_job( + job=job_body, + k8s_namespace=namespace, + ) + self.assertIsNotNone(submitted_job, "Job should be submitted successfully") + + result = self.api.wait_until_job_running( + job_name, namespace, timeout=120, delay_between_attempts=5 + ) + self.assertTrue( + result, + "Job should reach running state before suspension, completion, or failure", + ) + + stop_result = self.api.suspend_job(job_name, namespace) + self.assertTrue(stop_result, "Job should be suspended successfully") + + suspended = self.wait_for_job_status( + job_name, namespace, "Suspended", timeout=30 + ) + self.assertTrue( + suspended, "Job should reach Suspended status within 30 seconds" + ) + + resubmit_result = self.api.resubmit_job(job_name, namespace) + self.assertTrue(resubmit_result, "Job should be resubmitted successfully") + + result = self.api.wait_until_job_finished( + job_name, namespace, timeout=120, delay_between_attempts=5 + ) + self.assertTrue(result, "Resubmitted job should complete successfully") + + finally: + self.api.delete_job(job_name, namespace) + + def test_suspend_job_nonexistent(self): + """Test stopping a non-existent job.""" + result = self.api.suspend_job("nonexistent-job", namespace) + self.assertFalse(result, "Should return False for non-existent job") + + def test_resubmit_job_nonexistent(self): + """Test resubmitting a non-existent job.""" + result = self.api.resubmit_job("nonexistent-job", namespace) + self.assertFalse(result, "Should return False for non-existent job") + + def test_wait_until_job_running(self): + """Test waiting for a job to reach running state.""" + job_name = "wait-running-test-job" + + try: + job_body = create_job_with_ray_cluster_spec( + job_name=job_name, + namespace=namespace, + ) + + submitted_job = self.api.submit_job( + job=job_body, + k8s_namespace=namespace, + ) + self.assertIsNotNone(submitted_job, "Job should be submitted successfully") + + result = self.api.wait_until_job_running( + job_name, namespace, timeout=60, delay_between_attempts=3 + ) + self.assertTrue(result, "Job should reach running state") + + self.api.wait_until_job_finished(job_name, namespace, 60, 5) + + finally: + self.api.delete_job(job_name, namespace) + + def test_get_job(self): + """Test getting a job.""" + job_name = "get-test-job" + + try: + job_body = create_job_with_ray_cluster_spec( + job_name=job_name, + namespace=namespace, + ) + + submitted_job = self.api.submit_job( + job=job_body, + k8s_namespace=namespace, + ) + self.assertIsNotNone(submitted_job, "Job should be submitted successfully") + + status = self.api.get_job_status( + job_name, namespace, timeout=30, delay_between_attempts=2 + ) + self.assertIsNotNone(status, "Job status should be available") + + job = self.api.get_job(job_name, namespace) + self.assertIsNotNone(job, "Job should be retrieved successfully") + self.assertEqual(job["metadata"]["name"], job_name) + finally: + self.api.delete_job(job_name, namespace) + + def test_list_jobs(self): + """Test listing all jobs in a namespace.""" + created_jobs = [] + + try: + initial_result = self.api.list_jobs(k8s_namespace=namespace) + self.assertIsNotNone(initial_result, "List jobs should return a result") + self.assertIn( + "items", initial_result, "Result should contain 'items' field" + ) + initial_count = len(initial_result.get("items", [])) + + test_jobs = [ + {"name": "list-test-job-1", "type": "cluster_spec"}, + {"name": "list-test-job-2", "type": "cluster_spec"}, + {"name": "list-test-job-3", "type": "cluster_spec"}, + ] + + for job_info in test_jobs: + job_body = create_job_with_ray_cluster_spec( + job_name=job_info["name"], + namespace=namespace, + ) + + submitted_job = self.api.submit_job( + job=job_body, + k8s_namespace=namespace, + ) + self.assertIsNotNone( + submitted_job, + f"Job {job_info['name']} should be submitted successfully", + ) + created_jobs.append(job_info["name"]) + + status = self.api.get_job_status( + job_info["name"], namespace, timeout=10, delay_between_attempts=1 + ) + self.assertIsNotNone( + status, f"Job {job_info['name']} status should be available" + ) + + result = self.api.list_jobs(k8s_namespace=namespace) + self.assertIsNotNone(result, "List jobs should return a result") + self.assertIn("items", result, "Result should contain 'items' field") + + items = result.get("items", []) + current_count = len(items) + + self.assertGreaterEqual( + current_count, + initial_count + len(test_jobs), + f"Should have at least {len(test_jobs)} more jobs than initially", + ) + + job_names_in_list = [item.get("metadata", {}).get("name") for item in items] + for job_name in created_jobs: + self.assertIn( + job_name, job_names_in_list, f"Job {job_name} should be in the list" + ) + + finally: + for job_name in created_jobs: + try: + self.api.delete_job(job_name, namespace) + except Exception as e: + print(f"Failed to delete job {job_name}: {e}") + + def wait_for_job_status( + self, job_name, namespace, expected_status, timeout=60, check_interval=3 + ): + """Wait for a job to reach a specific status with polling.""" + start_time = time.time() + while time.time() - start_time < timeout: + status = self.api.get_job_status( + job_name, namespace, timeout=5, delay_between_attempts=1 + ) + current_status = status.get("jobDeploymentStatus") if status else None + + if current_status == expected_status: + return True + + time.sleep(check_interval) + + return False diff --git a/src/codeflare_sdk/vendored/python_client_test/test_utils.py b/src/codeflare_sdk/vendored/python_client_test/test_utils.py new file mode 100644 index 00000000..93d79db9 --- /dev/null +++ b/src/codeflare_sdk/vendored/python_client_test/test_utils.py @@ -0,0 +1,352 @@ +import unittest +import copy +from codeflare_sdk.vendored.python_client.utils import ( + kuberay_cluster_utils, + kuberay_cluster_builder, +) + + +test_cluster_body: dict = { + "apiVersion": "ray.io/v1", + "kind": "RayCluster", + "metadata": { + "labels": {"controller-tools.k8s.io": "1.0"}, + "name": "raycluster-complete-raw", + }, + "spec": { + "rayVersion": "2.46.0", + "headGroupSpec": { + "rayStartParams": {"dashboard-host": "0.0.0.0"}, + "template": { + "metadata": {"labels": {}}, + "spec": { + "containers": [ + { + "name": "ray-head", + "image": "rayproject/ray:2.46.0", + "ports": [ + {"containerPort": 6379, "name": "gcs"}, + {"containerPort": 8265, "name": "dashboard"}, + {"containerPort": 10001, "name": "client"}, + ], + "lifecycle": { + "preStop": { + "exec": {"command": ["/bin/sh", "-c", "ray stop"]} + } + }, + "volumeMounts": [ + {"mountPath": "/tmp/ray", "name": "ray-logs"} + ], + "resources": { + "limits": {"cpu": "1", "memory": "2G"}, + "requests": {"cpu": "500m", "memory": "2G"}, + }, + } + ], + "volumes": [{"name": "ray-logs", "emptyDir": {}}], + }, + }, + }, + "workerGroupSpecs": [ + { + "replicas": 1, + "minReplicas": 1, + "maxReplicas": 10, + "groupName": "small-group", + "rayStartParams": {}, + "template": { + "spec": { + "containers": [ + { + "name": "ray-worker", + "image": "rayproject/ray:2.46.0", + "lifecycle": { + "preStop": { + "exec": { + "command": ["/bin/sh", "-c", "ray stop"] + } + } + }, + "volumeMounts": [ + {"mountPath": "/tmp/ray", "name": "ray-logs"} + ], + "resources": { + "limits": {"cpu": "1", "memory": "1G"}, + "requests": {"cpu": "500m", "memory": "1G"}, + }, + }, + { + "name": "side-car", + "image": "rayproject/ray:2.46.0", + "resources": { + "limits": {"cpu": "1", "memory": "1G"}, + "requests": {"cpu": "500m", "memory": "1G"}, + }, + }, + ], + "volumes": [{"name": "ray-logs", "emptyDir": {}}], + } + }, + } + ], + }, + "status": { + "availableWorkerReplicas": 2, + "desiredWorkerReplicas": 1, + "endpoints": {"client": "10001", "dashboard": "8265", "gcs-server": "6379"}, + "head": {"serviceIP": "10.152.183.194"}, + "lastUpdateTime": "2023-02-16T05:15:17Z", + "maxWorkerReplicas": 2, + }, +} + + +class TestUtils(unittest.TestCase): + def __init__(self, methodName: str = ...) -> None: + super().__init__(methodName) + self.director = kuberay_cluster_builder.Director() + self.utils = kuberay_cluster_utils.ClusterUtils() + + def test_populate_worker_group(self): + worker_group, succeeded = self.utils.populate_worker_group( + group_name="small-group", + ray_image="rayproject/ray:2.46.0", + ray_command=["/bin/bash", "-lc"], + init_image="busybox:1.28", + cpu_requests="3", + memory_requests="1G", + cpu_limits="5", + memory_limits="10G", + replicas=1, + min_replicas=1, + max_replicas=3, + ray_start_params={"block": "True"}, + ) + self.assertIsNotNone(worker_group) + self.assertEqual(succeeded, True) + + self.assertEqual(worker_group["groupName"], "small-group") + self.assertEqual(worker_group["maxReplicas"], 3) + self.assertEqual(worker_group["minReplicas"], 1) + self.assertEqual(worker_group["rayStartParams"], {"block": "True"}) + self.assertEqual(worker_group["replicas"], 1) + + container = worker_group["template"]["spec"]["containers"][0] + self.assertEqual(container["image"], "rayproject/ray:2.46.0") + self.assertEqual(container["command"], ["/bin/bash", "-lc"]) + + resources = container["resources"] + self.assertEqual(resources["requests"]["cpu"], "3") + self.assertEqual(resources["requests"]["memory"], "1G") + self.assertEqual(resources["limits"]["cpu"], "5") + self.assertEqual(resources["limits"]["memory"], "10G") + + # min_replicas can be 0 and ray_start_params can be an empty dict. + worker_group, succeeded = self.utils.populate_worker_group( + group_name="small-group", + ray_image="rayproject/ray:2.46.0", + ray_command=["/bin/bash", "-lc"], + init_image="busybox:1.28", + cpu_requests="3", + memory_requests="1G", + cpu_limits="5", + memory_limits="10G", + replicas=1, + min_replicas=0, + max_replicas=3, + ray_start_params={}, + ) + self.assertIsNotNone(worker_group) + self.assertEqual(succeeded, True) + self.assertEqual(worker_group["rayStartParams"], {}) + self.assertEqual(worker_group["minReplicas"], 0) + + def test_update_worker_group_replicas(self): + cluster = self.director.build_small_cluster(name="small-cluster") + + actual = cluster["metadata"]["name"] + expected = "small-cluster" + self.assertEqual(actual, expected) + + cluster, succeeded = self.utils.update_worker_group_replicas( + cluster, + group_name="small-cluster-workers", + max_replicas=10, + min_replicas=1, + replicas=5, + ) + + self.assertEqual(succeeded, True) + + # testing the workergroup + actual = cluster["spec"]["workerGroupSpecs"][0]["replicas"] + expected = 5 + self.assertEqual(actual, expected) + + actual = cluster["spec"]["workerGroupSpecs"][0]["maxReplicas"] + expected = 10 + self.assertEqual(actual, expected) + + actual = cluster["spec"]["workerGroupSpecs"][0]["minReplicas"] + expected = 1 + self.assertEqual(actual, expected) + + def test_update_worker_group_resources(self): + cluster: dict = copy.deepcopy(test_cluster_body) + actual = cluster["metadata"]["name"] + expected = "raycluster-complete-raw" + self.assertEqual(actual, expected) + + cluster, succeeded = self.utils.update_worker_group_resources( + cluster, + group_name="small-group", + cpu_requests="3", + memory_requests="5G", + cpu_limits="5", + memory_limits="10G", + container_name="unspecified", + ) + self.assertEqual(succeeded, True) + self.assertEqual( + cluster["spec"]["workerGroupSpecs"][0]["template"]["spec"]["containers"][0][ + "resources" + ]["requests"]["cpu"], + "3", + ) + self.assertEqual( + cluster["spec"]["workerGroupSpecs"][0]["template"]["spec"]["containers"][1][ + "resources" + ]["requests"]["cpu"], + "500m", + ) + + cluster, succeeded = self.utils.update_worker_group_resources( + cluster, + group_name="small-group", + cpu_requests="4", + memory_requests="5G", + cpu_limits="5", + memory_limits="10G", + container_name="side-car", + ) + self.assertEqual(succeeded, True) + self.assertEqual( + cluster["spec"]["workerGroupSpecs"][0]["template"]["spec"]["containers"][1][ + "resources" + ]["requests"]["cpu"], + "4", + ) + + cluster, succeeded = self.utils.update_worker_group_resources( + cluster, + group_name="small-group", + cpu_requests="4", + memory_requests="15G", + cpu_limits="5", + memory_limits="25G", + container_name="all_containers", + ) + self.assertEqual(succeeded, True) + self.assertEqual( + cluster["spec"]["workerGroupSpecs"][0]["template"]["spec"]["containers"][1][ + "resources" + ]["requests"]["memory"], + "15G", + ) + + cluster, succeeded = self.utils.update_worker_group_resources( + cluster, + group_name="small-group", + cpu_requests="4", + memory_requests="15G", + cpu_limits="5", + memory_limits="25G", + container_name="wrong_name", + ) + self.assertEqual(succeeded, False) + + # missing parameter test + with self.assertRaises(TypeError): + cluster, succeeded = self.utils.update_worker_group_resources( + cluster, + group_name="small-group", + cpu_requests="4", + ) + + def test_duplicate_worker_group(self): + cluster = self.director.build_small_cluster(name="small-cluster") + actual = cluster["metadata"]["name"] + expected = "small-cluster" + self.assertEqual(actual, expected) + + cluster, succeeded = self.utils.duplicate_worker_group( + cluster, + group_name="small-cluster-workers", + new_group_name="new-small-group-workers", + ) + self.assertEqual(succeeded, True) + self.assertEqual( + cluster["spec"]["workerGroupSpecs"][1]["groupName"], + "new-small-group-workers", + ) + self.assertEqual( + cluster["spec"]["workerGroupSpecs"][1]["template"]["spec"]["containers"][0][ + "resources" + ]["requests"]["cpu"], + "1", + ) + + # missing parameter test + with self.assertRaises(TypeError): + cluster, succeeded = self.utils.duplicate_worker_group( + cluster, + group_name="small-cluster-workers", + ) + + def test_delete_worker_group(self): + """ + Test delete_worker_group + """ + cluster = self.director.build_small_cluster(name="small-cluster") + actual = cluster["metadata"]["name"] + expected = "small-cluster" + self.assertEqual(actual, expected) + + cluster, succeeded = self.utils.delete_worker_group( + cluster, + group_name="small-cluster-workers", + ) + self.assertEqual(succeeded, True) + self.assertEqual(len(cluster["spec"]["workerGroupSpecs"]), 0) + + # deleting the same worker group again should fail + with self.assertRaises(AssertionError): + cluster, succeeded = self.utils.delete_worker_group( + cluster, + group_name="small-cluster-workers", + ) + + def test_name(self): + self.assertEqual(self.utils.is_valid_name("name"), True) + self.assertEqual(self.utils.is_valid_name("name-"), False) + self.assertEqual(self.utils.is_valid_name(".name"), False) + self.assertEqual(self.utils.is_valid_name("name_something"), False) + self.assertEqual( + self.utils.is_valid_name( + "toooooooooooooooooooooooooooooooooooooooooo-loooooooooooooooooooong" + ), + False, + ) + + def test_label(self): + self.assertEqual(self.utils.is_valid_label("name"), True) + self.assertEqual(self.utils.is_valid_label("name-"), False) + self.assertEqual(self.utils.is_valid_label(".name"), False) + self.assertEqual(self.utils.is_valid_label("name_something"), True) + self.assertEqual(self.utils.is_valid_label("good.name"), True) + self.assertEqual( + self.utils.is_valid_label( + "toooooooooooooooooooooooooooooooooooooooooo-loooooooooooooooooooong" + ), + False, + ) diff --git a/tests/e2e/cluster_apply_kind_test.py b/tests/e2e/cluster_apply_kind_test.py new file mode 100644 index 00000000..e023e92d --- /dev/null +++ b/tests/e2e/cluster_apply_kind_test.py @@ -0,0 +1,119 @@ +from codeflare_sdk import Cluster, ClusterConfiguration +import pytest +import time +from kubernetes import client +from codeflare_sdk.common.utils import constants + +from support import ( + initialize_kubernetes_client, + create_namespace, + delete_namespace, + get_ray_cluster, +) + + +@pytest.mark.kind +class TestRayClusterApply: + def setup_method(self): + initialize_kubernetes_client(self) + + def teardown_method(self): + delete_namespace(self) + + def test_cluster_apply(self): + self.setup_method() + create_namespace(self) + + cluster_name = "test-cluster-apply" + namespace = self.namespace + + # Initial configuration with 1 worker + initial_config = ClusterConfiguration( + name=cluster_name, + namespace=namespace, + num_workers=1, + head_cpu_requests="500m", + head_cpu_limits="1", + head_memory_requests="1Gi", + head_memory_limits="2Gi", + worker_cpu_requests="500m", + worker_cpu_limits="1", + worker_memory_requests="1Gi", + worker_memory_limits="2Gi", + image=f"rayproject/ray:{constants.RAY_VERSION}", + write_to_file=True, + verify_tls=False, + ) + + # Create the cluster + cluster = Cluster(initial_config) + cluster.apply() + + # Wait for the cluster to be ready + cluster.wait_ready(dashboard_check=False) + status, ready = cluster.status() + assert ready, f"Cluster {cluster_name} is not ready: {status}" + + # Verify the cluster is created + ray_cluster = get_ray_cluster(cluster_name, namespace) + assert ray_cluster is not None, "Cluster was not created successfully" + assert ( + ray_cluster["spec"]["workerGroupSpecs"][0]["replicas"] == 1 + ), "Initial worker count does not match" + + # Update configuration with 2 workers + updated_config = ClusterConfiguration( + name=cluster_name, + namespace=namespace, + num_workers=2, + head_cpu_requests="500m", + head_cpu_limits="1", + head_memory_requests="1Gi", + head_memory_limits="2Gi", + worker_cpu_requests="500m", + worker_cpu_limits="1", + worker_memory_requests="1Gi", + worker_memory_limits="2Gi", + image=f"rayproject/ray:{constants.RAY_VERSION}", + write_to_file=True, + verify_tls=False, + ) + + # Apply the updated configuration + cluster.config = updated_config + cluster.apply() + + # Give Kubernetes a moment to process the update + time.sleep(5) + + # Wait for the updated cluster to be ready + cluster.wait_ready(dashboard_check=False) + updated_status, updated_ready = cluster.status() + assert ( + updated_ready + ), f"Cluster {cluster_name} is not ready after update: {updated_status}" + + # Verify the cluster is updated + updated_ray_cluster = get_ray_cluster(cluster_name, namespace) + assert ( + updated_ray_cluster["spec"]["workerGroupSpecs"][0]["replicas"] == 2 + ), "Worker count was not updated" + + # Clean up + cluster.down() + + # Wait for deletion to complete (finalizers may delay deletion) + max_wait = 30 # seconds + wait_interval = 2 + elapsed = 0 + + while elapsed < max_wait: + ray_cluster = get_ray_cluster(cluster_name, namespace) + if ray_cluster is None: + break + time.sleep(wait_interval) + elapsed += wait_interval + + assert ( + ray_cluster is None + ), f"Cluster was not deleted successfully after {max_wait}s" diff --git a/tests/e2e/heterogeneous_clusters_kind_test.py b/tests/e2e/heterogeneous_clusters_kind_test.py index 052fa7b8..fb650176 100644 --- a/tests/e2e/heterogeneous_clusters_kind_test.py +++ b/tests/e2e/heterogeneous_clusters_kind_test.py @@ -63,7 +63,7 @@ def run_heterogeneous_clusters( local_queue=queue_name, ) ) - cluster.up() + cluster.apply() sleep(5) node_name = get_pod_node(self, self.namespace, cluster_name) print(f"Cluster {cluster_name}-{flavor} is running on node: {node_name}") diff --git a/tests/e2e/heterogeneous_clusters_oauth_test.py b/tests/e2e/heterogeneous_clusters_oauth_test.py index d57cff48..0fbe4df3 100644 --- a/tests/e2e/heterogeneous_clusters_oauth_test.py +++ b/tests/e2e/heterogeneous_clusters_oauth_test.py @@ -55,9 +55,9 @@ def run_heterogeneous_clusters( namespace=self.namespace, name=cluster_name, num_workers=1, - head_cpu_requests="500m", - head_cpu_limits="500m", - worker_cpu_requests="500m", + head_cpu_requests=1, + head_cpu_limits=1, + worker_cpu_requests=1, worker_cpu_limits=1, worker_memory_requests=1, worker_memory_limits=4, @@ -66,7 +66,7 @@ def run_heterogeneous_clusters( local_queue=queue_name, ) ) - cluster.up() + cluster.apply() sleep(5) node_name = get_pod_node(self, self.namespace, cluster_name) print(f"Cluster {cluster_name}-{flavor} is running on node: {node_name}") diff --git a/tests/e2e/install-codeflare-sdk.sh b/tests/e2e/install-codeflare-sdk.sh index e7808582..8ec5e1e6 100644 --- a/tests/e2e/install-codeflare-sdk.sh +++ b/tests/e2e/install-codeflare-sdk.sh @@ -9,7 +9,7 @@ poetry config virtualenvs.create false cd codeflare-sdk # Lock dependencies and install them -poetry lock --no-update +poetry lock poetry install --with test,docs # Return to the workdir diff --git a/tests/e2e/local_interactive_sdk_kind_test.py b/tests/e2e/local_interactive_sdk_kind_test.py index c20fd879..1dd8a2e0 100644 --- a/tests/e2e/local_interactive_sdk_kind_test.py +++ b/tests/e2e/local_interactive_sdk_kind_test.py @@ -1,23 +1,31 @@ from codeflare_sdk import ( Cluster, ClusterConfiguration, - TokenAuthentication, generate_cert, ) import pytest import ray import math +import subprocess from support import * @pytest.mark.kind -class TestRayLocalInteractiveOauth: +class TestRayLocalInteractiveKind: def setup_method(self): initialize_kubernetes_client(self) + self.port_forward_process = None + + def cleanup_port_forward(self): + if self.port_forward_process: + self.port_forward_process.terminate() + self.port_forward_process.wait(timeout=10) + self.port_forward_process = None def teardown_method(self): + self.cleanup_port_forward() delete_namespace(self) delete_kueue_resources(self) @@ -39,6 +47,8 @@ def run_local_interactives( ): cluster_name = "test-ray-cluster-li" + ray.shutdown() + cluster = Cluster( ClusterConfiguration( name=cluster_name, @@ -46,28 +56,25 @@ def run_local_interactives( num_workers=1, head_cpu_requests="500m", head_cpu_limits="500m", - head_memory_requests=2, - head_memory_limits=2, worker_cpu_requests="500m", worker_cpu_limits=1, worker_memory_requests=1, worker_memory_limits=4, worker_extended_resource_requests={gpu_resource_name: number_of_gpus}, - write_to_file=True, verify_tls=False, ) ) - cluster.up() + + cluster.apply() + cluster.wait_ready() + cluster.status() generate_cert.generate_tls_cert(cluster_name, self.namespace) generate_cert.export_env(cluster_name, self.namespace) print(cluster.local_client_url()) - ray.shutdown() - ray.init(address=cluster.local_client_url(), logging_level="DEBUG") - @ray.remote(num_gpus=number_of_gpus / 2) def heavy_calculation_part(num_iterations): result = 0.0 @@ -84,10 +91,36 @@ def heavy_calculation(num_iterations): ) return sum(results) - ref = heavy_calculation.remote(3000) - result = ray.get(ref) - assert result == 1789.4644387076714 - ray.cancel(ref) - ray.shutdown() + # Attempt to port forward + try: + local_port = "20001" + ray_client_port = "10001" + + port_forward_cmd = [ + "kubectl", + "port-forward", + "-n", + self.namespace, + f"svc/{cluster_name}-head-svc", + f"{local_port}:{ray_client_port}", + ] + self.port_forward_process = subprocess.Popen( + port_forward_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + + client_url = f"ray://localhost:{local_port}" + cluster.status() + + ray.init(address=client_url, logging_level="INFO") + + ref = heavy_calculation.remote(3000) + result = ray.get(ref) + assert ( + result == 1789.4644387076728 + ) # Updated result after moving to Python 3.12 (0.0000000000008% difference to old assertion) + ray.cancel(ref) + ray.shutdown() - cluster.down() + cluster.down() + finally: + self.cleanup_port_forward() diff --git a/tests/e2e/local_interactive_sdk_oauth_test.py b/tests/e2e/local_interactive_sdk_oauth_test.py index b5229deb..8be0bf9c 100644 --- a/tests/e2e/local_interactive_sdk_oauth_test.py +++ b/tests/e2e/local_interactive_sdk_oauth_test.py @@ -44,6 +44,10 @@ def run_local_interactives(self): namespace=self.namespace, name=cluster_name, num_workers=1, + head_memory_requests=6, + head_memory_limits=8, + head_cpu_requests=1, + head_cpu_limits=1, worker_cpu_requests=1, worker_cpu_limits=1, worker_memory_requests=1, @@ -52,7 +56,7 @@ def run_local_interactives(self): verify_tls=False, ) ) - cluster.up() + cluster.apply() cluster.wait_ready() generate_cert.generate_tls_cert(cluster_name, self.namespace) diff --git a/tests/e2e/mnist_raycluster_sdk_aw_kind_test.py b/tests/e2e/mnist_raycluster_sdk_aw_kind_test.py index 4623a9e5..5d06214c 100644 --- a/tests/e2e/mnist_raycluster_sdk_aw_kind_test.py +++ b/tests/e2e/mnist_raycluster_sdk_aw_kind_test.py @@ -2,7 +2,7 @@ from time import sleep -from codeflare_sdk import Cluster, ClusterConfiguration, TokenAuthentication +from codeflare_sdk import Cluster, ClusterConfiguration from codeflare_sdk.ray.client import RayJobClient import pytest @@ -44,8 +44,6 @@ def run_mnist_raycluster_sdk_kind( num_workers=1, head_cpu_requests="500m", head_cpu_limits="500m", - head_memory_requests=2, - head_memory_limits=2, worker_cpu_requests="500m", worker_cpu_limits=1, worker_memory_requests=1, @@ -57,7 +55,7 @@ def run_mnist_raycluster_sdk_kind( ) ) - cluster.up() + cluster.apply() cluster.status() @@ -68,6 +66,9 @@ def run_mnist_raycluster_sdk_kind( cluster.details() self.assert_jobsubmit_withoutlogin_kind(cluster, accelerator, number_of_gpus) + assert_get_cluster_and_jobsubmit( + self, "mnist", accelerator="gpu", number_of_gpus=1 + ) # Assertions @@ -106,8 +107,6 @@ def assert_jobsubmit_withoutlogin_kind(self, cluster, accelerator, number_of_gpu client.delete_job(submission_id) - cluster.down() - def assert_job_completion(self, status): if status == "SUCCEEDED": print(f"Job has completed: '{status}'") diff --git a/tests/e2e/mnist_raycluster_sdk_kind_test.py b/tests/e2e/mnist_raycluster_sdk_kind_test.py index 6bfb19af..4ba728cf 100644 --- a/tests/e2e/mnist_raycluster_sdk_kind_test.py +++ b/tests/e2e/mnist_raycluster_sdk_kind_test.py @@ -2,7 +2,7 @@ from time import sleep -from codeflare_sdk import Cluster, ClusterConfiguration, TokenAuthentication +from codeflare_sdk import Cluster, ClusterConfiguration from codeflare_sdk.ray.client import RayJobClient import pytest @@ -44,8 +44,6 @@ def run_mnist_raycluster_sdk_kind( num_workers=1, head_cpu_requests="500m", head_cpu_limits="500m", - head_memory_requests=2, - head_memory_limits=2, worker_cpu_requests="500m", worker_cpu_limits=1, worker_memory_requests=1, @@ -56,7 +54,7 @@ def run_mnist_raycluster_sdk_kind( ) ) - cluster.up() + cluster.apply() cluster.status() @@ -68,6 +66,10 @@ def run_mnist_raycluster_sdk_kind( self.assert_jobsubmit_withoutlogin_kind(cluster, accelerator, number_of_gpus) + assert_get_cluster_and_jobsubmit( + self, "mnist", accelerator="gpu", number_of_gpus=1 + ) + # Assertions def assert_jobsubmit_withoutlogin_kind(self, cluster, accelerator, number_of_gpus): @@ -105,8 +107,6 @@ def assert_jobsubmit_withoutlogin_kind(self, cluster, accelerator, number_of_gpu client.delete_job(submission_id) - cluster.down() - def assert_job_completion(self, status): if status == "SUCCEEDED": print(f"Job has completed: '{status}'") diff --git a/tests/e2e/mnist_raycluster_sdk_oauth_test.py b/tests/e2e/mnist_raycluster_sdk_oauth_test.py index d3e69868..18447d74 100644 --- a/tests/e2e/mnist_raycluster_sdk_oauth_test.py +++ b/tests/e2e/mnist_raycluster_sdk_oauth_test.py @@ -2,7 +2,11 @@ from time import sleep -from codeflare_sdk import Cluster, ClusterConfiguration, TokenAuthentication +from codeflare_sdk import ( + Cluster, + ClusterConfiguration, + TokenAuthentication, +) from codeflare_sdk.ray.client import RayJobClient import pytest @@ -42,21 +46,19 @@ def run_mnist_raycluster_sdk_oauth(self): name="mnist", namespace=self.namespace, num_workers=1, - head_cpu_requests="500m", - head_cpu_limits="500m", - head_memory_requests=4, - head_memory_limits=4, + head_memory_requests=6, + head_memory_limits=8, worker_cpu_requests=1, worker_cpu_limits=1, - worker_memory_requests=1, - worker_memory_limits=4, + worker_memory_requests=6, + worker_memory_limits=8, image=ray_image, write_to_file=True, verify_tls=False, ) ) - cluster.up() + cluster.apply() cluster.status() @@ -68,6 +70,7 @@ def run_mnist_raycluster_sdk_oauth(self): self.assert_jobsubmit_withoutLogin(cluster) self.assert_jobsubmit_withlogin(cluster) + assert_get_cluster_and_jobsubmit(self, "mnist") # Assertions @@ -132,8 +135,6 @@ def assert_jobsubmit_withlogin(self, cluster): client.delete_job(submission_id) - cluster.down() - def assert_job_completion(self, status): if status == "SUCCEEDED": print(f"Job has completed: '{status}'") diff --git a/tests/e2e/rayjob/__init__.py b/tests/e2e/rayjob/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/rayjob/ray_version_validation_oauth_test.py b/tests/e2e/rayjob/ray_version_validation_oauth_test.py new file mode 100644 index 00000000..794d739a --- /dev/null +++ b/tests/e2e/rayjob/ray_version_validation_oauth_test.py @@ -0,0 +1,127 @@ +import pytest +import sys +import os + +# Add the parent directory to the path to import support +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from support import * + +from codeflare_sdk import ( + RayJob, + ManagedClusterConfig, +) + + +class TestRayJobRayVersionValidationOauth: + def setup_method(self): + initialize_kubernetes_client(self) + + def teardown_method(self): + delete_namespace(self) + delete_kueue_resources(self) + + def _create_basic_managed_cluster_config( + self, ray_image: str + ) -> ManagedClusterConfig: + """Helper method to create basic managed cluster configuration.""" + return ManagedClusterConfig( + head_cpu_requests="500m", + head_cpu_limits="500m", + head_memory_requests=1, + head_memory_limits=2, + num_workers=1, + worker_cpu_requests="500m", + worker_cpu_limits="500m", + worker_memory_requests=1, + worker_memory_limits=2, + image=ray_image, + ) + + def test_rayjob_lifecycled_cluster_incompatible_ray_version_oauth(self): + """Test that RayJob creation fails when cluster config specifies incompatible Ray version.""" + self.setup_method() + create_namespace(self) + create_kueue_resources(self) + self.run_rayjob_lifecycled_cluster_incompatible_version() + + def run_rayjob_lifecycled_cluster_incompatible_version(self): + """Test Ray version validation with cluster lifecycling using incompatible image.""" + + job_name = "incompatible-lifecycle-rayjob" + + # Create cluster configuration with incompatible Ray version (2.46.1 instead of expected 2.47.1) + incompatible_ray_image = "quay.io/modh/ray:2.46.1-py311-cu121" + + print( + f"Creating RayJob with incompatible Ray image in cluster config: {incompatible_ray_image}" + ) + + cluster_config = self._create_basic_managed_cluster_config( + incompatible_ray_image + ) + + # Create RayJob with incompatible cluster config - this should fail during submission + rayjob = RayJob( + job_name=job_name, + cluster_config=cluster_config, + namespace=self.namespace, + entrypoint="python -c 'print(\"This should not run due to version mismatch\")'", + ttl_seconds_after_finished=30, + ) + + print( + f"Attempting to submit RayJob '{job_name}' with incompatible Ray version..." + ) + + # This should fail during submission due to Ray version validation + with pytest.raises(ValueError, match="Ray version mismatch detected"): + rayjob.submit() + + print( + "✅ Ray version validation correctly prevented RayJob submission with incompatible cluster config!" + ) + + def test_rayjob_lifecycled_cluster_unknown_ray_version_oauth(self): + """Test that RayJob creation succeeds with warning when Ray version cannot be determined.""" + self.setup_method() + create_namespace(self) + create_kueue_resources(self) + self.run_rayjob_lifecycled_cluster_unknown_version() + + def run_rayjob_lifecycled_cluster_unknown_version(self): + """Test Ray version validation with unknown image (should warn but not fail).""" + + job_name = "unknown-version-rayjob" + + # Use an image where Ray version cannot be determined (SHA digest) + unknown_ray_image = "quay.io/modh/ray@sha256:6d076aeb38ab3c34a6a2ef0f58dc667089aa15826fa08a73273c629333e12f1e" + + print( + f"Creating RayJob with image where Ray version cannot be determined: {unknown_ray_image}" + ) + + cluster_config = self._create_basic_managed_cluster_config(unknown_ray_image) + + # Create RayJob with unknown version image - this should succeed with warning + rayjob = RayJob( + job_name=job_name, + cluster_config=cluster_config, + namespace=self.namespace, + entrypoint="python -c 'print(\"Testing unknown Ray version scenario\")'", + ttl_seconds_after_finished=30, + ) + + print(f"Attempting to submit RayJob '{job_name}' with unknown Ray version...") + + # This should succeed but with a warning + with pytest.warns(UserWarning, match="Cannot determine Ray version"): + submission_result = rayjob.submit() + + assert ( + submission_result == job_name + ), f"Job submission failed, expected {job_name}, got {submission_result}" + + print("✅ RayJob submission succeeded with warning for unknown Ray version!") + print( + f"Note: RayJob '{job_name}' was submitted successfully but may need manual cleanup." + ) diff --git a/tests/e2e/rayjob/rayjob_existing_cluster_test.py b/tests/e2e/rayjob/rayjob_existing_cluster_test.py new file mode 100644 index 00000000..8f6f0c3b --- /dev/null +++ b/tests/e2e/rayjob/rayjob_existing_cluster_test.py @@ -0,0 +1,119 @@ +import pytest +import sys +import os +from time import sleep + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from support import * + +from codeflare_sdk import ( + Cluster, + ClusterConfiguration, +) +from codeflare_sdk import RayJob, TokenAuthentication +from codeflare_sdk.ray.rayjobs.status import CodeflareRayJobStatus +from codeflare_sdk.vendored.python_client.kuberay_job_api import RayjobApi + + +class TestRayJobExistingCluster: + """Test RayJob against existing Kueue-managed clusters.""" + + def setup_method(self): + initialize_kubernetes_client(self) + + def teardown_method(self): + delete_namespace(self) + delete_kueue_resources(self) + + def test_existing_kueue_cluster(self): + """Test RayJob against Kueue-managed RayCluster.""" + self.setup_method() + create_namespace(self) + create_kueue_resources(self) + + cluster_name = "kueue-cluster" + + if is_openshift(): + auth = TokenAuthentication( + token=run_oc_command(["whoami", "--show-token=true"]), + server=run_oc_command(["whoami", "--show-server=true"]), + skip_tls=True, + ) + auth.login() + + resources = get_platform_appropriate_resources() + + cluster = Cluster( + ClusterConfiguration( + name=cluster_name, + namespace=self.namespace, + num_workers=1, + head_cpu_requests=resources["head_cpu_requests"], + head_cpu_limits=resources["head_cpu_limits"], + head_memory_requests=resources["head_memory_requests"], + head_memory_limits=resources["head_memory_limits"], + worker_cpu_requests=resources["worker_cpu_requests"], + worker_cpu_limits=resources["worker_cpu_limits"], + worker_memory_requests=resources["worker_memory_requests"], + worker_memory_limits=resources["worker_memory_limits"], + image=constants.CUDA_PY312_RUNTIME_IMAGE, + local_queue=self.local_queues[0], + write_to_file=True, + verify_tls=False, + ) + ) + + cluster.apply() + + # Wait for cluster to be ready (with Kueue admission) + print(f"Waiting for cluster '{cluster_name}' to be ready...") + cluster.wait_ready(timeout=600) + print(f"✓ Cluster '{cluster_name}' is ready") + + # RayJob with explicit local_queue + rayjob_explicit = RayJob( + job_name="job-explicit-queue", + cluster_name=cluster_name, + namespace=self.namespace, + entrypoint="python -c \"import ray; ray.init(); print('Job with explicit queue')\"", + runtime_env={"env_vars": get_setup_env_variables(ACCELERATOR="cpu")}, + local_queue=self.local_queues[0], + ) + + # RayJob using default queue + rayjob_default = RayJob( + job_name="job-default-queue", + cluster_name=cluster_name, + namespace=self.namespace, + entrypoint="python -c \"import ray; ray.init(); print('Job with default queue')\"", + runtime_env={"env_vars": get_setup_env_variables(ACCELERATOR="cpu")}, + ) + + try: + # Test RayJob with explicit queue + assert rayjob_explicit.submit() == "job-explicit-queue" + self._wait_completion(rayjob_explicit) + + # Test RayJob with default queue + assert rayjob_default.submit() == "job-default-queue" + self._wait_completion(rayjob_default) + finally: + rayjob_explicit.delete() + rayjob_default.delete() + cluster.down() + + def _wait_completion(self, rayjob: RayJob, timeout: int = 600): + """Wait for RayJob completion.""" + elapsed = 0 + interval = 10 + + while elapsed < timeout: + status, _ = rayjob.status(print_to_console=False) + if status == CodeflareRayJobStatus.COMPLETE: + return + elif status == CodeflareRayJobStatus.FAILED: + raise AssertionError(f"RayJob '{rayjob.name}' failed") + sleep(interval) + elapsed += interval + + raise TimeoutError(f"RayJob '{rayjob.name}' timeout after {timeout}s") diff --git a/tests/e2e/rayjob/rayjob_lifecycled_cluster_test.py b/tests/e2e/rayjob/rayjob_lifecycled_cluster_test.py new file mode 100644 index 00000000..2256f06f --- /dev/null +++ b/tests/e2e/rayjob/rayjob_lifecycled_cluster_test.py @@ -0,0 +1,249 @@ +import pytest +import sys +import os +from time import sleep +import tempfile + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from support import * + +from codeflare_sdk import RayJob, ManagedClusterConfig + +from kubernetes import client +from codeflare_sdk.vendored.python_client.kuberay_job_api import RayjobApi +from codeflare_sdk.vendored.python_client.kuberay_cluster_api import RayClusterApi + + +class TestRayJobLifecycledCluster: + """Test RayJob with auto-created cluster lifecycle management.""" + + def setup_method(self): + initialize_kubernetes_client(self) + + def teardown_method(self): + delete_namespace(self) + delete_kueue_resources(self) + + def test_lifecycled_kueue_managed(self): + """Test RayJob with Kueue-managed lifecycled cluster with Secret validation.""" + self.setup_method() + create_namespace(self) + create_kueue_resources(self) + + self.job_api = RayjobApi() + cluster_api = RayClusterApi() + job_name = "kueue-lifecycled" + + # Get platform-appropriate resource configurations + resources = get_platform_appropriate_resources() + + cluster_config = ManagedClusterConfig( + head_cpu_requests=resources["head_cpu_requests"], + head_cpu_limits=resources["head_cpu_limits"], + head_memory_requests=resources["head_memory_requests"], + head_memory_limits=resources["head_memory_limits"], + num_workers=1, + worker_cpu_requests=resources["worker_cpu_requests"], + worker_cpu_limits=resources["worker_cpu_limits"], + worker_memory_requests=resources["worker_memory_requests"], + worker_memory_limits=resources["worker_memory_limits"], + ) + + # Create a temporary script file to test Secret functionality + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, dir=os.getcwd() + ) as script_file: + script_file.write( + """ + import ray + ray.init() + print('Kueue job with Secret done') + ray.shutdown() + """ + ) + script_file.flush() + script_filename = os.path.basename(script_file.name) + + try: + rayjob = RayJob( + job_name=job_name, + namespace=self.namespace, + cluster_config=cluster_config, + entrypoint=f"python {script_filename}", + runtime_env={"env_vars": get_setup_env_variables(ACCELERATOR="cpu")}, + local_queue=self.local_queues[0], + ) + + assert rayjob.submit() == job_name + + # Verify Secret was created with owner reference + self.verify_secret_with_owner_reference(rayjob) + + assert self.job_api.wait_until_job_running( + name=rayjob.name, k8s_namespace=rayjob.namespace, timeout=600 + ) + + assert self.job_api.wait_until_job_finished( + name=rayjob.name, k8s_namespace=rayjob.namespace, timeout=300 + ) + finally: + try: + rayjob.delete() + except Exception: + pass # Job might already be deleted + verify_rayjob_cluster_cleanup(cluster_api, rayjob.name, rayjob.namespace) + # Clean up the temporary script file + if "script_filename" in locals(): + try: + os.remove(script_filename) + except: + pass + + def test_lifecycled_kueue_resource_queueing(self): + """Test Kueue resource queueing with lifecycled clusters.""" + self.setup_method() + create_namespace(self) + create_limited_kueue_resources(self) + + self.job_api = RayjobApi() + cluster_api = RayClusterApi() + + # Get platform-appropriate resource configurations + resources = get_platform_appropriate_resources() + + cluster_config = ManagedClusterConfig( + head_cpu_requests=resources["head_cpu_requests"], + head_cpu_limits=resources["head_cpu_limits"], + head_memory_requests=resources["head_memory_requests"], + head_memory_limits=resources["head_memory_limits"], + num_workers=0, + ) + + job1 = None + job2 = None + try: + job1 = RayJob( + job_name="holder", + namespace=self.namespace, + cluster_config=cluster_config, + entrypoint='python -c "import ray; import time; ray.init(); time.sleep(15)"', + runtime_env={"env_vars": get_setup_env_variables(ACCELERATOR="cpu")}, + local_queue=self.local_queues[0], + ) + assert job1.submit() == "holder" + assert self.job_api.wait_until_job_running( + name=job1.name, k8s_namespace=job1.namespace, timeout=60 + ) + + job2 = RayJob( + job_name="waiter", + namespace=self.namespace, + cluster_config=cluster_config, + entrypoint='python -c "import ray; ray.init()"', + runtime_env={"env_vars": get_setup_env_variables(ACCELERATOR="cpu")}, + local_queue=self.local_queues[0], + ) + assert job2.submit() == "waiter" + + # Wait for Kueue to process the job + sleep(5) + job2_cr = self.job_api.get_job(name=job2.name, k8s_namespace=job2.namespace) + + # For RayJobs with managed clusters, check if Kueue is holding resources + job2_status = job2_cr.get("status", {}) + ray_cluster_name = job2_status.get("rayClusterName", "") + + # If RayCluster is not created yet, it means Kueue is holding the job + if not ray_cluster_name: + # This is the expected behavior + job_is_queued = True + else: + # Check RayCluster resources - if all are 0, it's queued + ray_cluster_status = job2_status.get("rayClusterStatus", {}) + desired_cpu = ray_cluster_status.get("desiredCPU", "0") + desired_memory = ray_cluster_status.get("desiredMemory", "0") + + # Kueue creates the RayCluster but with 0 resources when queued + job_is_queued = desired_cpu == "0" and desired_memory == "0" + + assert job_is_queued, "Job2 should be queued by Kueue while Job1 is running" + + assert self.job_api.wait_until_job_finished( + name=job1.name, k8s_namespace=job1.namespace, timeout=60 + ) + + assert wait_for_kueue_admission( + self, self.job_api, job2.name, job2.namespace, timeout=30 + ) + + assert self.job_api.wait_until_job_finished( + name=job2.name, k8s_namespace=job2.namespace, timeout=60 + ) + finally: + for job in [job1, job2]: + if job: + try: + job.delete() + verify_rayjob_cluster_cleanup( + cluster_api, job.name, job.namespace + ) + except: + pass + + def verify_secret_with_owner_reference(self, rayjob: RayJob): + """Verify that the Secret was created with proper owner reference to the RayJob.""" + v1 = client.CoreV1Api() + secret_name = f"{rayjob.name}-files" + + try: + # Get the Secret + secret = v1.read_namespaced_secret( + name=secret_name, namespace=rayjob.namespace + ) + + # Verify Secret exists + assert secret is not None, f"Secret {secret_name} not found" + + # Verify it contains the script + assert secret.data is not None, "Secret has no data" + assert len(secret.data) > 0, "Secret data is empty" + + # Verify owner reference + assert ( + secret.metadata.owner_references is not None + ), "Secret has no owner references" + assert ( + len(secret.metadata.owner_references) > 0 + ), "Secret owner references list is empty" + + owner_ref = secret.metadata.owner_references[0] + assert ( + owner_ref.api_version == "ray.io/v1" + ), f"Wrong API version: {owner_ref.api_version}" + assert owner_ref.kind == "RayJob", f"Wrong kind: {owner_ref.kind}" + assert owner_ref.name == rayjob.name, f"Wrong owner name: {owner_ref.name}" + assert ( + owner_ref.controller is True + ), "Owner reference controller not set to true" + assert ( + owner_ref.block_owner_deletion is True + ), "Owner reference blockOwnerDeletion not set to true" + + # Verify labels + assert secret.metadata.labels.get("ray.io/job-name") == rayjob.name + assert ( + secret.metadata.labels.get("app.kubernetes.io/managed-by") + == "codeflare-sdk" + ) + assert ( + secret.metadata.labels.get("app.kubernetes.io/component") + == "rayjob-files" + ) + + print(f"✓ Secret {secret_name} verified with proper owner reference") + + except client.rest.ApiException as e: + if e.status == 404: + raise AssertionError(f"Secret {secret_name} not found") + else: + raise e diff --git a/tests/e2e/start_ray_cluster.py b/tests/e2e/start_ray_cluster.py index 8aac19f0..bc7f531f 100644 --- a/tests/e2e/start_ray_cluster.py +++ b/tests/e2e/start_ray_cluster.py @@ -26,7 +26,7 @@ ) ) -cluster.up() +cluster.apply() cluster.status() diff --git a/tests/e2e/support.py b/tests/e2e/support.py index d76b460c..85b3dd35 100644 --- a/tests/e2e/support.py +++ b/tests/e2e/support.py @@ -1,18 +1,109 @@ -import json import os import random import string import subprocess +from time import sleep +from codeflare_sdk import get_cluster from kubernetes import client, config -import kubernetes.client from codeflare_sdk.common.kubernetes_cluster.kube_api_helpers import ( _kube_api_error_handling, ) +from codeflare_sdk.common.utils import constants +from codeflare_sdk.common.utils.utils import get_ray_image_for_python_version + + +def get_ray_cluster(cluster_name, namespace): + api = client.CustomObjectsApi() + try: + return api.get_namespaced_custom_object( + group="ray.io", + version="v1", + namespace=namespace, + plural="rayclusters", + name=cluster_name, + ) + except client.exceptions.ApiException as e: + if e.status == 404: + return None + raise + + +def is_openshift(): + """Detect if running on OpenShift by checking for OpenShift-specific API resources.""" + try: + api = client.ApiClient() + discovery = client.ApisApi(api) + # Check for OpenShift-specific API group + groups = discovery.get_api_versions().groups + for group in groups: + if group.name == "image.openshift.io": + return True + return False + except Exception: + # If we can't determine, assume it's not OpenShift + return False def get_ray_image(): - default_ray_image = "quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06" - return os.getenv("RAY_IMAGE", default_ray_image) + """ + Get appropriate Ray image based on platform (OpenShift vs Kind/vanilla K8s). + + The tests marked with @pytest.mark.openshift can run on both OpenShift and Kind clusters + with Kueue installed. This function automatically selects the appropriate image: + - OpenShift: Uses the CUDA runtime image (quay.io/modh/ray:...) + - Kind/K8s: Uses the standard Ray image (rayproject/ray:VERSION) + + You can override this behavior by setting the RAY_IMAGE environment variable. + """ + # Allow explicit override via environment variable + if "RAY_IMAGE" in os.environ: + return os.environ["RAY_IMAGE"] + + # Auto-detect platform and return appropriate image + if is_openshift(): + return get_ray_image_for_python_version() + else: + # Use standard Ray image for Kind/vanilla K8s + return f"rayproject/ray:{constants.RAY_VERSION}" + + +def get_platform_appropriate_resources(): + """ + Get appropriate resource configurations based on platform. + + OpenShift with MODH images requires more memory than Kind with standard Ray images. + + Returns: + dict: Resource configurations with keys: + - head_cpu_requests, head_cpu_limits + - head_memory_requests, head_memory_limits + - worker_cpu_requests, worker_cpu_limits + - worker_memory_requests, worker_memory_limits + """ + if is_openshift(): + # MODH runtime images require more memory + return { + "head_cpu_requests": "1", + "head_cpu_limits": "1.5", + "head_memory_requests": 7, + "head_memory_limits": 8, + "worker_cpu_requests": "1", + "worker_cpu_limits": "1", + "worker_memory_requests": 5, + "worker_memory_limits": 6, + } + else: + # Standard Ray images require less memory + return { + "head_cpu_requests": "1", + "head_cpu_limits": "1.5", + "head_memory_requests": 7, + "head_memory_limits": 8, + "worker_cpu_requests": "1", + "worker_cpu_limits": "1", + "worker_memory_requests": 2, + "worker_memory_limits": 3, + } def get_setup_env_variables(**kwargs): @@ -128,6 +219,17 @@ def run_oc_command(args): return None +def run_kubectl_command(args): + try: + result = subprocess.run( + ["kubectl"] + args, capture_output=True, text=True, check=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"Error executing 'kubectl {' '.join(args)}': {e}") + return None + + def create_cluster_queue(self, cluster_queue, flavor): cluster_queue_json = { "apiVersion": "kueue.x-k8s.io/v1beta1", @@ -142,9 +244,9 @@ def create_cluster_queue(self, cluster_queue, flavor): { "name": flavor, "resources": [ - {"name": "cpu", "nominalQuota": 9}, - {"name": "memory", "nominalQuota": "36Gi"}, - {"name": "nvidia.com/gpu", "nominalQuota": 1}, + {"name": "cpu", "nominalQuota": 20}, + {"name": "memory", "nominalQuota": "80Gi"}, + {"name": "nvidia.com/gpu", "nominalQuota": 2}, ], }, ], @@ -282,7 +384,6 @@ def create_kueue_resources( def delete_kueue_resources(self): - # Delete if given cluster-queue exists for cq in self.cluster_queues: try: self.custom_api.delete_cluster_custom_object( @@ -348,3 +449,276 @@ def get_nodes_by_label(self, node_labels): label_selector = ",".join(f"{k}={v}" for k, v in node_labels.items()) nodes = self.api_instance.list_node(label_selector=label_selector) return [node.metadata.name for node in nodes.items] + + +def assert_get_cluster_and_jobsubmit( + self, cluster_name, accelerator=None, number_of_gpus=None +): + # Retrieve the cluster + cluster = get_cluster(cluster_name, self.namespace, False) + + cluster.details() + + # Initialize the job client + client = cluster.job_client + + # Submit a job and get the submission ID + env_vars = ( + get_setup_env_variables(ACCELERATOR=accelerator) + if accelerator + else get_setup_env_variables() + ) + submission_id = client.submit_job( + entrypoint="python mnist.py", + runtime_env={ + "working_dir": "./tests/e2e/", + "pip": "./tests/e2e/mnist_pip_requirements.txt", + "env_vars": env_vars, + }, + entrypoint_num_cpus=1 if number_of_gpus is None else None, + entrypoint_num_gpus=number_of_gpus, + ) + print(f"Submitted job with ID: {submission_id}") + + # Fetch the list of jobs and validate + job_list = client.list_jobs() + print(f"List of Jobs: {job_list}") + + # Validate the number of jobs in the list + assert len(job_list) == 1 + + # Validate the submission ID matches + assert job_list[0].submission_id == submission_id + + cluster.down() + + +def wait_for_kueue_admission(self, job_api, job_name, namespace, timeout=120): + print(f"Waiting for Kueue admission of job '{job_name}'...") + elapsed_time = 0 + check_interval = 5 + + while elapsed_time < timeout: + try: + job_cr = job_api.get_job(name=job_name, k8s_namespace=namespace) + + # Check if the job is no longer suspended + is_suspended = job_cr.get("spec", {}).get("suspend", False) + + if not is_suspended: + print(f"✓ Job '{job_name}' admitted by Kueue (no longer suspended)") + return True + + # Debug: Check workload status every 10 seconds + if elapsed_time % 10 == 0: + workload = get_kueue_workload_for_job(self, job_name, namespace) + if workload: + conditions = workload.get("status", {}).get("conditions", []) + print(f" DEBUG: Workload conditions for '{job_name}':") + for condition in conditions: + print( + f" - {condition.get('type')}: {condition.get('status')} - {condition.get('reason', '')} - {condition.get('message', '')}" + ) + + # Optional: Check status conditions for more details + conditions = job_cr.get("status", {}).get("conditions", []) + for condition in conditions: + if ( + condition.get("type") == "Suspended" + and condition.get("status") == "False" + ): + print( + f"✓ Job '{job_name}' admitted by Kueue (Suspended=False condition)" + ) + return True + + except Exception as e: + print(f"Error checking job status: {e}") + + sleep(check_interval) + elapsed_time += check_interval + + print(f"✗ Timeout waiting for Kueue admission of job '{job_name}'") + return False + + +def create_limited_kueue_resources(self): + print("Creating limited Kueue resources for preemption testing...") + + # Create a resource flavor with default (no special labels/tolerations) + resource_flavor = f"limited-flavor-{random_choice()}" + create_resource_flavor( + self, resource_flavor, default=True, with_labels=False, with_tolerations=False + ) + self.resource_flavors = [resource_flavor] + + # Create a cluster queue with very limited resources + # Adjust quota based on platform - OpenShift needs more memory + if is_openshift(): + # MODH images need more memory, so higher quota but still limited to allow only 1 job + cpu_quota = 3 + memory_quota = "15Gi" # One job needs ~8Gi head, allow some buffer + else: + # Standard Ray images - one job needs ~8G head + 500m submitter + cpu_quota = 3 + memory_quota = "10Gi" # Enough for one job (8G head + submitter), but not two + + cluster_queue_name = f"limited-cq-{random_choice()}" + cluster_queue_json = { + "apiVersion": "kueue.x-k8s.io/v1beta1", + "kind": "ClusterQueue", + "metadata": {"name": cluster_queue_name}, + "spec": { + "namespaceSelector": {}, + "resourceGroups": [ + { + "coveredResources": ["cpu", "memory"], + "flavors": [ + { + "name": resource_flavor, + "resources": [ + { + "name": "cpu", + "nominalQuota": cpu_quota, + }, + { + "name": "memory", + "nominalQuota": memory_quota, + }, + ], + } + ], + } + ], + }, + } + + try: + self.custom_api.create_cluster_custom_object( + group="kueue.x-k8s.io", + plural="clusterqueues", + version="v1beta1", + body=cluster_queue_json, + ) + print(f"✓ Created limited ClusterQueue: {cluster_queue_name}") + except Exception as e: + print(f"Error creating limited ClusterQueue: {e}") + raise + + self.cluster_queues = [cluster_queue_name] + + # Create a local queue + local_queue_name = f"limited-lq-{random_choice()}" + create_local_queue(self, cluster_queue_name, local_queue_name, is_default=True) + self.local_queues = [local_queue_name] + + print("✓ Limited Kueue resources created successfully") + + +def get_kueue_workload_for_job(self, job_name, namespace): + try: + # List all workloads in the namespace + workloads = self.custom_api.list_namespaced_custom_object( + group="kueue.x-k8s.io", + version="v1beta1", + plural="workloads", + namespace=namespace, + ) + + # Find workload with matching RayJob owner reference + for workload in workloads.get("items", []): + owner_refs = workload.get("metadata", {}).get("ownerReferences", []) + + for owner_ref in owner_refs: + if ( + owner_ref.get("kind") == "RayJob" + and owner_ref.get("name") == job_name + ): + workload_name = workload.get("metadata", {}).get("name") + print( + f"✓ Found Kueue workload '{workload_name}' for RayJob '{job_name}'" + ) + return workload + + print(f"✗ No Kueue workload found for RayJob '{job_name}'") + return None + + except Exception as e: + print(f"Error getting Kueue workload for job '{job_name}': {e}") + return None + + +def wait_for_job_status( + job_api, rayjob_name: str, namespace: str, expected_status: str, timeout: int = 30 +) -> bool: + """ + Wait for a RayJob to reach a specific deployment status. + + Args: + job_api: RayjobApi instance + rayjob_name: Name of the RayJob + namespace: Namespace of the RayJob + expected_status: Expected jobDeploymentStatus value + timeout: Maximum time to wait in seconds + + Returns: + bool: True if status reached, False if timeout + """ + elapsed_time = 0 + check_interval = 2 + + while elapsed_time < timeout: + status = job_api.get_job_status(name=rayjob_name, k8s_namespace=namespace) + if status and status.get("jobDeploymentStatus") == expected_status: + return True + + sleep(check_interval) + elapsed_time += check_interval + + return False + + +def verify_rayjob_cluster_cleanup( + cluster_api, rayjob_name: str, namespace: str, timeout: int = 60 +): + """ + Verify that the RayCluster created by a RayJob has been cleaned up. + Handles KubeRay's automatic suffix addition to cluster names. + + Args: + cluster_api: RayClusterApi instance + rayjob_name: Name of the RayJob + namespace: Namespace to check + timeout: Maximum time to wait in seconds + + Raises: + TimeoutError: If cluster is not cleaned up within timeout + """ + elapsed_time = 0 + check_interval = 5 + + while elapsed_time < timeout: + # List all RayClusters in the namespace + clusters = cluster_api.list_ray_clusters( + k8s_namespace=namespace, async_req=False + ) + + # Check if any cluster exists that starts with our job name + found = False + for cluster in clusters.get("items", []): + cluster_name = cluster.get("metadata", {}).get("name", "") + # KubeRay creates clusters with pattern: {job_name}-raycluster-{suffix} + if cluster_name.startswith(f"{rayjob_name}-raycluster"): + found = True + break + + if not found: + # No cluster found, cleanup successful + return + + sleep(check_interval) + elapsed_time += check_interval + + raise TimeoutError( + f"RayCluster for job '{rayjob_name}' was not cleaned up within {timeout} seconds" + ) diff --git a/tests/test_cluster_yamls/appwrapper/test-case-bad.yaml b/tests/test_cluster_yamls/appwrapper/test-case-bad.yaml index 9166eced..a5915820 100644 --- a/tests/test_cluster_yamls/appwrapper/test-case-bad.yaml +++ b/tests/test_cluster_yamls/appwrapper/test-case-bad.yaml @@ -42,7 +42,7 @@ spec: valueFrom: fieldRef: fieldPath: status.podIP - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + image: "${image}" imagePullPolicy: IfNotPresent lifecycle: preStop: @@ -66,7 +66,7 @@ spec: requests: cpu: 2 memory: 8G - rayVersion: 2.35.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-unit-test-cluster maxReplicas: 2 @@ -89,7 +89,7 @@ spec: valueFrom: fieldRef: fieldPath: status.podIP - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + image: "${image}" lifecycle: preStop: exec: diff --git a/tests/test_cluster_yamls/appwrapper/unit-test-all-params.yaml b/tests/test_cluster_yamls/appwrapper/unit-test-all-params.yaml index 6d2c5440..fe07e331 100644 --- a/tests/test_cluster_yamls/appwrapper/unit-test-all-params.yaml +++ b/tests/test_cluster_yamls/appwrapper/unit-test-all-params.yaml @@ -13,10 +13,13 @@ spec: metadata: annotations: app.kubernetes.io/managed-by: test-prefix + key1: value1 + key2: value2 labels: controller-tools.k8s.io: '1.0' key1: value1 key2: value2 + ray.io/cluster: aw-all-params name: aw-all-params namespace: ns spec: @@ -40,6 +43,11 @@ spec: resources: '"{\"TPU\": 2}"' serviceType: ClusterIP template: + metadata: + annotations: + app.kubernetes.io/managed-by: test-prefix + key1: value1 + key2: value2 spec: containers: - env: @@ -47,6 +55,8 @@ spec: value: value1 - name: key2 value: value2 + - name: RAY_USAGE_STATS_ENABLED + value: '0' image: example/ray:tag imagePullPolicy: Always lifecycle: @@ -76,6 +86,12 @@ spec: memory: 12G nvidia.com/gpu: 1 volumeMounts: + - mountPath: /home/ray/test1 + name: test + - mountPath: /home/ray/test2 + name: test2 + - mountPath: /home/ray/test2 + name: test3 - mountPath: /etc/pki/tls/certs/odh-trusted-ca-bundle.crt name: odh-trusted-ca-cert subPath: odh-trusted-ca-bundle.crt @@ -91,7 +107,24 @@ spec: imagePullSecrets: - name: secret1 - name: secret2 + tolerations: + - effect: NoSchedule + key: key1 + operator: Equal + value: value1 volumes: + - emptyDir: + sizeLimit: 500Gi + name: test + - configMap: + items: + - key: test + path: /home/ray/test2/data.txt + name: config-map-test + name: test2 + - name: test3 + secret: + secretName: test-secret - configMap: items: - key: ca-bundle.crt @@ -106,7 +139,7 @@ spec: name: odh-trusted-ca-bundle optional: true name: odh-ca-cert - rayVersion: 2.35.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-aw-all-params maxReplicas: 10 @@ -117,6 +150,11 @@ spec: resources: '"{}"' replicas: 10 template: + metadata: + annotations: + app.kubernetes.io/managed-by: test-prefix + key1: value1 + key2: value2 spec: containers: - env: @@ -124,6 +162,8 @@ spec: value: value1 - name: key2 value: value2 + - name: RAY_USAGE_STATS_ENABLED + value: '0' image: example/ray:tag imagePullPolicy: Always lifecycle: @@ -144,6 +184,12 @@ spec: memory: 12G nvidia.com/gpu: 1 volumeMounts: + - mountPath: /home/ray/test1 + name: test + - mountPath: /home/ray/test2 + name: test2 + - mountPath: /home/ray/test2 + name: test3 - mountPath: /etc/pki/tls/certs/odh-trusted-ca-bundle.crt name: odh-trusted-ca-cert subPath: odh-trusted-ca-bundle.crt @@ -159,7 +205,24 @@ spec: imagePullSecrets: - name: secret1 - name: secret2 + tolerations: + - effect: NoSchedule + key: key2 + operator: Equal + value: value2 volumes: + - emptyDir: + sizeLimit: 500Gi + name: test + - configMap: + items: + - key: test + path: /home/ray/test2/data.txt + name: config-map-test + name: test2 + - name: test3 + secret: + secretName: test-secret - configMap: items: - key: ca-bundle.crt diff --git a/tests/test_cluster_yamls/kueue/aw_kueue.yaml b/tests/test_cluster_yamls/kueue/aw_kueue.yaml index 402ffb6a..92e5078d 100644 --- a/tests/test_cluster_yamls/kueue/aw_kueue.yaml +++ b/tests/test_cluster_yamls/kueue/aw_kueue.yaml @@ -13,6 +13,7 @@ spec: metadata: labels: controller-tools.k8s.io: '1.0' + ray.io/cluster: unit-test-aw-kueue name: unit-test-aw-kueue namespace: ns spec: @@ -38,7 +39,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: Always lifecycle: preStop: @@ -75,6 +76,9 @@ spec: - mountPath: /etc/ssl/certs/odh-ca-bundle.crt name: odh-ca-cert subPath: odh-ca-bundle.crt + env: + - name: RAY_USAGE_STATS_ENABLED + value: '0' volumes: - configMap: items: @@ -90,7 +94,7 @@ spec: name: odh-trusted-ca-bundle optional: true name: odh-ca-cert - rayVersion: 2.35.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-unit-test-aw-kueue maxReplicas: 2 @@ -103,7 +107,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: Always lifecycle: preStop: @@ -133,6 +137,9 @@ spec: - mountPath: /etc/ssl/certs/odh-ca-bundle.crt name: odh-ca-cert subPath: odh-ca-bundle.crt + env: + - name: RAY_USAGE_STATS_ENABLED + value: '0' volumes: - configMap: items: diff --git a/tests/test_cluster_yamls/kueue/ray_cluster_kueue.yaml b/tests/test_cluster_yamls/kueue/ray_cluster_kueue.yaml index a5cb3616..04331aed 100644 --- a/tests/test_cluster_yamls/kueue/ray_cluster_kueue.yaml +++ b/tests/test_cluster_yamls/kueue/ray_cluster_kueue.yaml @@ -13,6 +13,7 @@ spec: metadata: labels: controller-tools.k8s.io: '1.0' + ray.io/cluster: unit-test-cluster-kueue name: unit-test-cluster-kueue namespace: ns spec: @@ -38,7 +39,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: Always lifecycle: preStop: @@ -75,6 +76,9 @@ spec: - mountPath: /etc/ssl/certs/odh-ca-bundle.crt name: odh-ca-cert subPath: odh-ca-bundle.crt + env: + - name: RAY_USAGE_STATS_ENABLED + value: '0' volumes: - configMap: items: @@ -90,7 +94,7 @@ spec: name: odh-trusted-ca-bundle optional: true name: odh-ca-cert - rayVersion: 2.35.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-unit-test-cluster-kueue maxReplicas: 2 @@ -103,7 +107,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: Always lifecycle: preStop: @@ -133,6 +137,9 @@ spec: - mountPath: /etc/ssl/certs/odh-ca-bundle.crt name: odh-ca-cert subPath: odh-ca-bundle.crt + env: + - name: RAY_USAGE_STATS_ENABLED + value: '0' volumes: - configMap: items: diff --git a/tests/test_cluster_yamls/ray/default-appwrapper.yaml b/tests/test_cluster_yamls/ray/default-appwrapper.yaml index 3e97474d..1041f3b5 100644 --- a/tests/test_cluster_yamls/ray/default-appwrapper.yaml +++ b/tests/test_cluster_yamls/ray/default-appwrapper.yaml @@ -11,6 +11,7 @@ spec: metadata: labels: controller-tools.k8s.io: '1.0' + ray.io/cluster: default-appwrapper name: default-appwrapper namespace: ns spec: @@ -36,7 +37,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: Always lifecycle: preStop: @@ -53,6 +54,9 @@ spec: name: dashboard - containerPort: 10001 name: client + env: + - name: RAY_USAGE_STATS_ENABLED + value: '0' resources: limits: cpu: 2 @@ -88,7 +92,7 @@ spec: name: odh-trusted-ca-bundle optional: true name: odh-ca-cert - rayVersion: 2.35.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-default-appwrapper maxReplicas: 1 @@ -101,7 +105,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: Always lifecycle: preStop: @@ -111,6 +115,9 @@ spec: - -c - ray stop name: machine-learning + env: + - name: RAY_USAGE_STATS_ENABLED + value: '0' resources: limits: cpu: 1 diff --git a/tests/test_cluster_yamls/ray/default-ray-cluster.yaml b/tests/test_cluster_yamls/ray/default-ray-cluster.yaml index 34de53d2..213b22cf 100644 --- a/tests/test_cluster_yamls/ray/default-ray-cluster.yaml +++ b/tests/test_cluster_yamls/ray/default-ray-cluster.yaml @@ -3,6 +3,7 @@ kind: RayCluster metadata: labels: controller-tools.k8s.io: '1.0' + ray.io/cluster: default-cluster name: default-cluster namespace: ns spec: @@ -28,7 +29,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: Always lifecycle: preStop: @@ -45,6 +46,9 @@ spec: name: dashboard - containerPort: 10001 name: client + env: + - name: RAY_USAGE_STATS_ENABLED + value: '0' resources: limits: cpu: 2 @@ -80,7 +84,7 @@ spec: name: odh-trusted-ca-bundle optional: true name: odh-ca-cert - rayVersion: 2.35.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-default-cluster maxReplicas: 1 @@ -93,7 +97,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: Always lifecycle: preStop: @@ -110,6 +114,9 @@ spec: requests: cpu: 1 memory: 2G + env: + - name: RAY_USAGE_STATS_ENABLED + value: '0' volumeMounts: - mountPath: /etc/pki/tls/certs/odh-trusted-ca-bundle.crt name: odh-trusted-ca-cert diff --git a/tests/test_cluster_yamls/ray/unit-test-all-params.yaml b/tests/test_cluster_yamls/ray/unit-test-all-params.yaml index 8426eede..7c7d82d6 100644 --- a/tests/test_cluster_yamls/ray/unit-test-all-params.yaml +++ b/tests/test_cluster_yamls/ray/unit-test-all-params.yaml @@ -3,11 +3,14 @@ kind: RayCluster metadata: annotations: app.kubernetes.io/managed-by: test-prefix + key1: value1 + key2: value2 labels: controller-tools.k8s.io: '1.0' key1: value1 key2: value2 kueue.x-k8s.io/queue-name: local-queue-default + ray.io/cluster: test-all-params name: test-all-params namespace: ns spec: @@ -31,6 +34,11 @@ spec: resources: '"{\"TPU\": 2}"' serviceType: ClusterIP template: + metadata: + annotations: + app.kubernetes.io/managed-by: test-prefix + key1: value1 + key2: value2 spec: containers: - env: @@ -38,6 +46,8 @@ spec: value: value1 - name: key2 value: value2 + - name: RAY_USAGE_STATS_ENABLED + value: '0' image: example/ray:tag imagePullPolicy: Always lifecycle: @@ -67,6 +77,12 @@ spec: memory: 12G nvidia.com/gpu: 1 volumeMounts: + - mountPath: /home/ray/test1 + name: test + - mountPath: /home/ray/test2 + name: test2 + - mountPath: /home/ray/test2 + name: test3 - mountPath: /etc/pki/tls/certs/odh-trusted-ca-bundle.crt name: odh-trusted-ca-cert subPath: odh-trusted-ca-bundle.crt @@ -82,7 +98,24 @@ spec: imagePullSecrets: - name: secret1 - name: secret2 + tolerations: + - effect: NoSchedule + key: key1 + operator: Equal + value: value1 volumes: + - emptyDir: + sizeLimit: 500Gi + name: test + - configMap: + items: + - key: test + path: /home/ray/test2/data.txt + name: config-map-test + name: test2 + - name: test3 + secret: + secretName: test-secret - configMap: items: - key: ca-bundle.crt @@ -97,7 +130,7 @@ spec: name: odh-trusted-ca-bundle optional: true name: odh-ca-cert - rayVersion: 2.35.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-test-all-params maxReplicas: 10 @@ -108,6 +141,11 @@ spec: resources: '"{}"' replicas: 10 template: + metadata: + annotations: + app.kubernetes.io/managed-by: test-prefix + key1: value1 + key2: value2 spec: containers: - env: @@ -115,6 +153,8 @@ spec: value: value1 - name: key2 value: value2 + - name: RAY_USAGE_STATS_ENABLED + value: '0' image: example/ray:tag imagePullPolicy: Always lifecycle: @@ -135,6 +175,12 @@ spec: memory: 12G nvidia.com/gpu: 1 volumeMounts: + - mountPath: /home/ray/test1 + name: test + - mountPath: /home/ray/test2 + name: test2 + - mountPath: /home/ray/test2 + name: test3 - mountPath: /etc/pki/tls/certs/odh-trusted-ca-bundle.crt name: odh-trusted-ca-cert subPath: odh-trusted-ca-bundle.crt @@ -150,7 +196,24 @@ spec: imagePullSecrets: - name: secret1 - name: secret2 + tolerations: + - effect: NoSchedule + key: key2 + operator: Equal + value: value2 volumes: + - emptyDir: + sizeLimit: 500Gi + name: test + - configMap: + items: + - key: test + path: /home/ray/test2/data.txt + name: config-map-test + name: test2 + - name: test3 + secret: + secretName: test-secret - configMap: items: - key: ca-bundle.crt diff --git a/tests/test_cluster_yamls/support_clusters/test-aw-a.yaml b/tests/test_cluster_yamls/support_clusters/test-aw-a.yaml index fe26900d..49f2c38c 100644 --- a/tests/test_cluster_yamls/support_clusters/test-aw-a.yaml +++ b/tests/test_cluster_yamls/support_clusters/test-aw-a.yaml @@ -38,7 +38,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: IfNotPresent lifecycle: preStop: @@ -91,7 +91,7 @@ spec: name: odh-trusted-ca-bundle optional: true name: odh-ca-cert - rayVersion: 2.35.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-test-cluster-a maxReplicas: 1 @@ -109,7 +109,7 @@ spec: key: value spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" lifecycle: preStop: exec: diff --git a/tests/test_cluster_yamls/support_clusters/test-aw-b.yaml b/tests/test_cluster_yamls/support_clusters/test-aw-b.yaml index eed571fe..aa6fad9c 100644 --- a/tests/test_cluster_yamls/support_clusters/test-aw-b.yaml +++ b/tests/test_cluster_yamls/support_clusters/test-aw-b.yaml @@ -38,7 +38,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: IfNotPresent lifecycle: preStop: @@ -91,7 +91,7 @@ spec: name: odh-trusted-ca-bundle optional: true name: odh-ca-cert - rayVersion: 2.35.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-test-cluster-b maxReplicas: 1 @@ -109,7 +109,7 @@ spec: key: value spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" lifecycle: preStop: exec: diff --git a/tests/test_cluster_yamls/support_clusters/test-rc-a.yaml b/tests/test_cluster_yamls/support_clusters/test-rc-a.yaml index 5f5d456c..2bb13995 100644 --- a/tests/test_cluster_yamls/support_clusters/test-rc-a.yaml +++ b/tests/test_cluster_yamls/support_clusters/test-rc-a.yaml @@ -29,7 +29,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: IfNotPresent lifecycle: preStop: @@ -82,7 +82,7 @@ spec: name: odh-trusted-ca-bundle optional: true name: odh-ca-cert - rayVersion: 2.35.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-test-cluster-a maxReplicas: 1 @@ -100,7 +100,7 @@ spec: key: value spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" lifecycle: preStop: exec: diff --git a/tests/test_cluster_yamls/support_clusters/test-rc-b.yaml b/tests/test_cluster_yamls/support_clusters/test-rc-b.yaml index 3bf894db..70f1d5bf 100644 --- a/tests/test_cluster_yamls/support_clusters/test-rc-b.yaml +++ b/tests/test_cluster_yamls/support_clusters/test-rc-b.yaml @@ -29,7 +29,7 @@ spec: template: spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" imagePullPolicy: IfNotPresent lifecycle: preStop: @@ -82,7 +82,7 @@ spec: name: odh-trusted-ca-bundle optional: true name: odh-ca-cert - rayVersion: 2.35.0 + rayVersion: 2.47.1 workerGroupSpecs: - groupName: small-group-test-rc-b maxReplicas: 1 @@ -100,7 +100,7 @@ spec: key: value spec: containers: - - image: quay.io/modh/ray@sha256:0d715f92570a2997381b7cafc0e224cfa25323f18b9545acfd23bc2b71576d06 + - image: "${image}" lifecycle: preStop: exec: diff --git a/tests/upgrade/raycluster_sdk_upgrade_sleep_test.py b/tests/upgrade/raycluster_sdk_upgrade_sleep_test.py index 793853d0..c61f5d19 100644 --- a/tests/upgrade/raycluster_sdk_upgrade_sleep_test.py +++ b/tests/upgrade/raycluster_sdk_upgrade_sleep_test.py @@ -68,7 +68,7 @@ def run_mnist_raycluster_sdk_oauth(self): ) try: - cluster.up() + cluster.apply() cluster.status() # wait for raycluster to be Ready cluster.wait_ready() diff --git a/tests/upgrade/raycluster_sdk_upgrade_test.py b/tests/upgrade/raycluster_sdk_upgrade_test.py index 7c8b2922..80fd105f 100644 --- a/tests/upgrade/raycluster_sdk_upgrade_test.py +++ b/tests/upgrade/raycluster_sdk_upgrade_test.py @@ -17,7 +17,7 @@ # Creates a Ray cluster -class TestMNISTRayClusterUp: +class TestMNISTRayClusterApply: def setup_method(self): initialize_kubernetes_client(self) create_namespace_with_name(self, namespace) @@ -50,12 +50,12 @@ def run_mnist_raycluster_sdk_oauth(self): num_workers=1, head_cpu_requests=1, head_cpu_limits=1, - head_memory_requests=4, - head_memory_limits=4, + head_memory_requests=6, + head_memory_limits=8, worker_cpu_requests=1, worker_cpu_limits=1, - worker_memory_requests=4, - worker_memory_limits=4, + worker_memory_requests=6, + worker_memory_limits=8, image=ray_image, write_to_file=True, verify_tls=False, @@ -63,7 +63,7 @@ def run_mnist_raycluster_sdk_oauth(self): ) try: - cluster.up() + cluster.apply() cluster.status() # wait for raycluster to be Ready cluster.wait_ready() diff --git a/ui-tests/tests/widget_notebook_example.test.ts b/ui-tests/tests/widget_notebook_example.test.ts index d37c225c..7707f70b 100644 --- a/ui-tests/tests/widget_notebook_example.test.ts +++ b/ui-tests/tests/widget_notebook_example.test.ts @@ -67,25 +67,25 @@ test.describe("Visual Regression", () => { // At this point, all cells have been ran, and their screenshots have been captured. // We now interact with the widgets in the notebook. - const upDownWidgetCellIndex = 3; // 4 on OpenShift + const applyDownWidgetCellIndex = 3; // 4 on OpenShift - await waitForWidget(page, upDownWidgetCellIndex, 'input[type="checkbox"]'); - await waitForWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Down")'); - await waitForWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Up")'); + await waitForWidget(page, applyDownWidgetCellIndex, 'input[type="checkbox"]'); + await waitForWidget(page, applyDownWidgetCellIndex, 'button:has-text("Cluster Down")'); + await waitForWidget(page, applyDownWidgetCellIndex, 'button:has-text("Cluster Apply")'); - await interactWithWidget(page, upDownWidgetCellIndex, 'input[type="checkbox"]', async (checkbox) => { + await interactWithWidget(page, applyDownWidgetCellIndex, 'input[type="checkbox"]', async (checkbox) => { await checkbox.click(); const isChecked = await checkbox.isChecked(); expect(isChecked).toBe(true); }); - await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Down")', async (button) => { + await interactWithWidget(page, applyDownWidgetCellIndex, 'button:has-text("Cluster Down")', async (button) => { await button.click(); - const clusterDownMessage = await page.waitForSelector('text=No instances found, nothing to be done.', { timeout: 5000 }); - expect(clusterDownMessage).not.toBeNull(); + const clusterDownMessage = await page.waitForSelector('text=The requested resource could not be located.', { timeout: 5000 }); + expect(await clusterDownMessage.innerText()).toContain('The requested resource could not be located.'); }); - await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Up")', async (button) => { + await interactWithWidget(page, applyDownWidgetCellIndex, 'button:has-text("Cluster Apply")', async (button) => { await button.click(); const successMessage = await page.waitForSelector('text=Ray Cluster: \'widgettest\' has successfully been created', { timeout: 10000 }); @@ -103,7 +103,7 @@ test.describe("Visual Regression", () => { await runPreviousCell(page, cellCount, '(, True)'); - await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Down")', async (button) => { + await interactWithWidget(page, applyDownWidgetCellIndex, 'button:has-text("Cluster Down")', async (button) => { await button.click(); const clusterDownMessage = await page.waitForSelector('text=Ray Cluster: \'widgettest\' has successfully been deleted', { timeout: 5000 }); expect(clusterDownMessage).not.toBeNull(); @@ -116,7 +116,7 @@ test.describe("Visual Regression", () => { await cell.fill('"widgettest-1"'); await page.notebook.runCell(cellCount - 3, true); // Run ClusterConfiguration cell - await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Up")', async (button) => { + await interactWithWidget(page, applyDownWidgetCellIndex, 'button:has-text("Cluster Apply")', async (button) => { await button.click(); const successMessage = await page.waitForSelector('text=Ray Cluster: \'widgettest-1\' has successfully been created', { timeout: 10000 }); expect(successMessage).not.toBeNull(); diff --git a/ui-tests/yarn.lock b/ui-tests/yarn.lock index cf0b5b0c..bf9629eb 100644 --- a/ui-tests/yarn.lock +++ b/ui-tests/yarn.lock @@ -2059,9 +2059,9 @@ ms@2.0.0: integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + version "3.3.8" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== node-fetch@^2.6.7: version "2.7.0"