From bb84b678c059461555b168b317af9542e9065769 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:13:59 +0000 Subject: [PATCH 1/2] feat: move the Code Interpreter template and e2b-charts into the monorepo Co-Authored-By: mish@e2b.dev --- .changeset/jupyter-unix-socket.md | 5 + .github/scripts/build_release_itinerary.cjs | 2 + .github/workflows/charts_python_tests.yml | 41 + .../code_interpreter_template_build.yml | 209 ++++ .../code_interpreter_template_tests.yml | 133 +++ .github/workflows/publish_packages.yml | 8 + .github/workflows/release.yml | 45 + .github/workflows/sdk_tests.yml | 29 + README.md | 4 + packages/charts-python/LICENSE | 9 + packages/charts-python/README.md | 3 + packages/charts-python/e2b_charts/__init__.py | 1 + .../e2b_charts/charts/__init__.py | 4 + .../charts-python/e2b_charts/charts/bars.py | 140 +++ .../charts-python/e2b_charts/charts/base.py | 75 ++ .../charts-python/e2b_charts/charts/pie.py | 36 + .../charts-python/e2b_charts/charts/planar.py | 132 +++ packages/charts-python/e2b_charts/main.py | 119 +++ .../e2b_charts/utils/__init__.py | 1 + .../e2b_charts/utils/filtering.py | 16 + .../e2b_charts/utils/rounding.py | 13 + packages/charts-python/package.json | 13 + packages/charts-python/pyproject.toml | 38 + .../charts-python/tests/charts/test_bar.py | 49 + .../charts-python/tests/charts/test_blank.py | 9 + .../tests/charts/test_box_and_whiskers.py | 57 ++ .../tests/charts/test_categorical_scale.py | 25 + .../tests/charts/test_datetime_scale.py | 30 + .../charts-python/tests/charts/test_line.py | 74 ++ .../tests/charts/test_log_graph.py | 63 ++ .../charts-python/tests/charts/test_pie.py | 53 + .../tests/charts/test_scatter.py | 58 ++ .../tests/charts/test_supergraph.py | 66 ++ .../tests/charts/test_unknown.py | 37 + .../tests/utils/test_detect_scale.py | 24 + .../tests/utils/test_is_grid_line.py | 17 + packages/charts-python/uv.lock | 889 ++++++++++++++++ packages/code-interpreter-template/Makefile | 8 + packages/code-interpreter-template/README.md | 126 +++ .../code-interpreter-template/build_ci.py | 19 + .../code-interpreter-template/build_debug.py | 23 + .../code-interpreter-template/build_docker.py | 5 + .../code-interpreter-template/build_prod.py | 18 + .../code-interpreter-template/build_test.py | 13 + .../code-interpreter-template/debug_logs.py | 35 + .../ipython_kernel_config.py | 969 ++++++++++++++++++ .../jupyter-healthcheck.sh | 23 + .../jupyter_server_config.py | 67 ++ .../code-interpreter-template/matplotlibrc | 1 + .../code-interpreter-template/package.json | 9 + .../requirements-dev.txt | 2 + .../requirements.txt | 44 + .../server/api/models/__init__.py | 0 .../server/api/models/context.py | 11 + .../server/api/models/create_context.py | 13 + .../server/api/models/env_vars.py | 4 + .../server/api/models/error.py | 17 + .../server/api/models/execution_request.py | 16 + .../server/api/models/logs.py | 19 + .../server/api/models/output.py | 31 + .../server/api/models/result.py | 105 ++ .../server/consts.py | 2 + .../server/contexts.py | 72 ++ .../code-interpreter-template/server/envs.py | 20 + .../server/errors.py | 2 + .../code-interpreter-template/server/main.py | 219 ++++ .../server/messaging.py | 612 +++++++++++ .../server/requirements.txt | 6 + .../server/stream.py | 40 + .../server/utils/locks.py | 18 + .../code-interpreter-template/start-up.sh | 18 + .../startup_scripts/0002_data.py | 85 ++ .../startup_scripts/0003_images.py | 24 + .../systemd/code-interpreter.service | 17 + .../systemd/jupyter-debug.conf | 5 + .../systemd/jupyter.service | 17 + .../code-interpreter-template/template.py | 146 +++ .../tests/.env.example | 7 + .../code-interpreter-template/tests/README.md | 22 + .../tests/charts/__init__.py | 0 .../tests/charts/conftest.py | 15 + .../tests/charts/test_bar.py | 43 + .../tests/charts/test_box_and_whiskers.py | 53 + .../tests/charts/test_line.py | 70 ++ .../tests/charts/test_log_chart.py | 55 + .../tests/charts/test_pie.py | 43 + .../tests/charts/test_scale.py | 42 + .../tests/charts/test_scatter.py | 50 + .../tests/charts/test_superchart.py | 55 + .../tests/charts/test_unknown.py | 33 + .../tests/conftest.py | 117 +++ .../tests/env_vars/__init__.py | 0 .../tests/env_vars/test_bash.py | 30 + .../tests/env_vars/test_java.py | 46 + .../tests/env_vars/test_js.py | 43 + .../tests/env_vars/test_python.py | 40 + .../tests/env_vars/test_r.py | 38 + .../tests/harness/__init__.py | 17 + .../tests/harness/code_interpreter.py | 303 ++++++ .../tests/pyproject.toml | 28 + .../tests/test_async.py | 46 + .../tests/test_basic.py | 75 ++ .../tests/test_contexts.py | 118 +++ .../tests/test_cwd.py | 43 + .../tests/test_interrupt.py | 20 + .../tests/test_jupyter_socket.py | 42 + .../tests/test_kernels.py | 52 + .../tests/test_killed.py | 42 + .../tests/test_results.py | 111 ++ .../tests/test_streaming.py | 70 ++ .../tests/test_systemd.py | 45 + .../code-interpreter-template/tests/uv.lock | 602 +++++++++++ pnpm-lock.yaml | 4 + 113 files changed, 7828 insertions(+) create mode 100644 .changeset/jupyter-unix-socket.md create mode 100644 .github/workflows/charts_python_tests.yml create mode 100644 .github/workflows/code_interpreter_template_build.yml create mode 100644 .github/workflows/code_interpreter_template_tests.yml create mode 100644 packages/charts-python/LICENSE create mode 100644 packages/charts-python/README.md create mode 100644 packages/charts-python/e2b_charts/__init__.py create mode 100644 packages/charts-python/e2b_charts/charts/__init__.py create mode 100644 packages/charts-python/e2b_charts/charts/bars.py create mode 100644 packages/charts-python/e2b_charts/charts/base.py create mode 100644 packages/charts-python/e2b_charts/charts/pie.py create mode 100644 packages/charts-python/e2b_charts/charts/planar.py create mode 100644 packages/charts-python/e2b_charts/main.py create mode 100644 packages/charts-python/e2b_charts/utils/__init__.py create mode 100644 packages/charts-python/e2b_charts/utils/filtering.py create mode 100644 packages/charts-python/e2b_charts/utils/rounding.py create mode 100644 packages/charts-python/package.json create mode 100644 packages/charts-python/pyproject.toml create mode 100644 packages/charts-python/tests/charts/test_bar.py create mode 100644 packages/charts-python/tests/charts/test_blank.py create mode 100644 packages/charts-python/tests/charts/test_box_and_whiskers.py create mode 100644 packages/charts-python/tests/charts/test_categorical_scale.py create mode 100644 packages/charts-python/tests/charts/test_datetime_scale.py create mode 100644 packages/charts-python/tests/charts/test_line.py create mode 100644 packages/charts-python/tests/charts/test_log_graph.py create mode 100644 packages/charts-python/tests/charts/test_pie.py create mode 100644 packages/charts-python/tests/charts/test_scatter.py create mode 100644 packages/charts-python/tests/charts/test_supergraph.py create mode 100644 packages/charts-python/tests/charts/test_unknown.py create mode 100644 packages/charts-python/tests/utils/test_detect_scale.py create mode 100644 packages/charts-python/tests/utils/test_is_grid_line.py create mode 100644 packages/charts-python/uv.lock create mode 100644 packages/code-interpreter-template/Makefile create mode 100644 packages/code-interpreter-template/README.md create mode 100644 packages/code-interpreter-template/build_ci.py create mode 100644 packages/code-interpreter-template/build_debug.py create mode 100644 packages/code-interpreter-template/build_docker.py create mode 100644 packages/code-interpreter-template/build_prod.py create mode 100644 packages/code-interpreter-template/build_test.py create mode 100644 packages/code-interpreter-template/debug_logs.py create mode 100644 packages/code-interpreter-template/ipython_kernel_config.py create mode 100644 packages/code-interpreter-template/jupyter-healthcheck.sh create mode 100644 packages/code-interpreter-template/jupyter_server_config.py create mode 100644 packages/code-interpreter-template/matplotlibrc create mode 100644 packages/code-interpreter-template/package.json create mode 100644 packages/code-interpreter-template/requirements-dev.txt create mode 100644 packages/code-interpreter-template/requirements.txt create mode 100644 packages/code-interpreter-template/server/api/models/__init__.py create mode 100644 packages/code-interpreter-template/server/api/models/context.py create mode 100644 packages/code-interpreter-template/server/api/models/create_context.py create mode 100644 packages/code-interpreter-template/server/api/models/env_vars.py create mode 100644 packages/code-interpreter-template/server/api/models/error.py create mode 100644 packages/code-interpreter-template/server/api/models/execution_request.py create mode 100644 packages/code-interpreter-template/server/api/models/logs.py create mode 100644 packages/code-interpreter-template/server/api/models/output.py create mode 100644 packages/code-interpreter-template/server/api/models/result.py create mode 100644 packages/code-interpreter-template/server/consts.py create mode 100644 packages/code-interpreter-template/server/contexts.py create mode 100644 packages/code-interpreter-template/server/envs.py create mode 100644 packages/code-interpreter-template/server/errors.py create mode 100644 packages/code-interpreter-template/server/main.py create mode 100644 packages/code-interpreter-template/server/messaging.py create mode 100644 packages/code-interpreter-template/server/requirements.txt create mode 100644 packages/code-interpreter-template/server/stream.py create mode 100644 packages/code-interpreter-template/server/utils/locks.py create mode 100755 packages/code-interpreter-template/start-up.sh create mode 100644 packages/code-interpreter-template/startup_scripts/0002_data.py create mode 100644 packages/code-interpreter-template/startup_scripts/0003_images.py create mode 100644 packages/code-interpreter-template/systemd/code-interpreter.service create mode 100644 packages/code-interpreter-template/systemd/jupyter-debug.conf create mode 100644 packages/code-interpreter-template/systemd/jupyter.service create mode 100644 packages/code-interpreter-template/template.py create mode 100644 packages/code-interpreter-template/tests/.env.example create mode 100644 packages/code-interpreter-template/tests/README.md create mode 100644 packages/code-interpreter-template/tests/charts/__init__.py create mode 100644 packages/code-interpreter-template/tests/charts/conftest.py create mode 100644 packages/code-interpreter-template/tests/charts/test_bar.py create mode 100644 packages/code-interpreter-template/tests/charts/test_box_and_whiskers.py create mode 100644 packages/code-interpreter-template/tests/charts/test_line.py create mode 100644 packages/code-interpreter-template/tests/charts/test_log_chart.py create mode 100644 packages/code-interpreter-template/tests/charts/test_pie.py create mode 100644 packages/code-interpreter-template/tests/charts/test_scale.py create mode 100644 packages/code-interpreter-template/tests/charts/test_scatter.py create mode 100644 packages/code-interpreter-template/tests/charts/test_superchart.py create mode 100644 packages/code-interpreter-template/tests/charts/test_unknown.py create mode 100644 packages/code-interpreter-template/tests/conftest.py create mode 100644 packages/code-interpreter-template/tests/env_vars/__init__.py create mode 100644 packages/code-interpreter-template/tests/env_vars/test_bash.py create mode 100644 packages/code-interpreter-template/tests/env_vars/test_java.py create mode 100644 packages/code-interpreter-template/tests/env_vars/test_js.py create mode 100644 packages/code-interpreter-template/tests/env_vars/test_python.py create mode 100644 packages/code-interpreter-template/tests/env_vars/test_r.py create mode 100644 packages/code-interpreter-template/tests/harness/__init__.py create mode 100644 packages/code-interpreter-template/tests/harness/code_interpreter.py create mode 100644 packages/code-interpreter-template/tests/pyproject.toml create mode 100644 packages/code-interpreter-template/tests/test_async.py create mode 100644 packages/code-interpreter-template/tests/test_basic.py create mode 100644 packages/code-interpreter-template/tests/test_contexts.py create mode 100644 packages/code-interpreter-template/tests/test_cwd.py create mode 100644 packages/code-interpreter-template/tests/test_interrupt.py create mode 100644 packages/code-interpreter-template/tests/test_jupyter_socket.py create mode 100644 packages/code-interpreter-template/tests/test_kernels.py create mode 100644 packages/code-interpreter-template/tests/test_killed.py create mode 100644 packages/code-interpreter-template/tests/test_results.py create mode 100644 packages/code-interpreter-template/tests/test_streaming.py create mode 100644 packages/code-interpreter-template/tests/test_systemd.py create mode 100644 packages/code-interpreter-template/tests/uv.lock diff --git a/.changeset/jupyter-unix-socket.md b/.changeset/jupyter-unix-socket.md new file mode 100644 index 0000000000..b5b2ed6c3b --- /dev/null +++ b/.changeset/jupyter-unix-socket.md @@ -0,0 +1,5 @@ +--- +'@e2b/code-interpreter-template': patch +--- + +Isolate Jupyter behind a private Unix socket (`/run/e2b-jupyter/server.sock`, mode `0600`) instead of TCP port 8888, so Jupyter's HTTP API, kernel WebSockets, and interrupts are no longer reachable over the sandbox network interface. diff --git a/.github/scripts/build_release_itinerary.cjs b/.github/scripts/build_release_itinerary.cjs index ae6cb4e2f8..601c79265d 100644 --- a/.github/scripts/build_release_itinerary.cjs +++ b/.github/scripts/build_release_itinerary.cjs @@ -13,6 +13,8 @@ const labels = { '@e2b/cli': 'CLI (@e2b/cli)', '@e2b/dockerfile-utils': 'Dockerfile Utils JS (@e2b/dockerfile-utils)', '@e2b/dockerfile-utils-python': 'Dockerfile Utils Python (e2b-dockerfile-utils)', + '@e2b/charts-python': 'Charts Python (e2b-charts)', + '@e2b/code-interpreter-template': 'Code Interpreter template (code-interpreter-v1)', } const statusFile = process.argv[2] diff --git a/.github/workflows/charts_python_tests.yml b/.github/workflows/charts_python_tests.yml new file mode 100644 index 0000000000..5da555cc4a --- /dev/null +++ b/.github/workflows/charts_python_tests.yml @@ -0,0 +1,41 @@ +name: Test Charts Python + +on: + workflow_call: + +permissions: + contents: read + +jobs: + test: + defaults: + run: + working-directory: ./packages/charts-python + name: Charts Python - Build and test + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Parse .tool-versions + uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1 + with: + filename: '.tool-versions' + uppercase: 'true' + prefix: 'tool_version_' + + - name: Install uv + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + with: + version: '${{ env.TOOL_VERSION_UV }}' + python-version: '${{ env.TOOL_VERSION_PYTHON }}' + enable-cache: true + + - name: Install dependencies + run: uv sync --locked + + - name: Test build + run: uv build + + - name: Run tests + run: uv run pytest --verbose -x diff --git a/.github/workflows/code_interpreter_template_build.yml b/.github/workflows/code_interpreter_template_build.yml new file mode 100644 index 0000000000..316e1a52e3 --- /dev/null +++ b/.github/workflows/code_interpreter_template_build.yml @@ -0,0 +1,209 @@ +name: Build Code Interpreter Template + +on: + workflow_dispatch: + inputs: + target_environment: + description: Target environment + required: true + type: choice + default: foxtrot + options: + - foxtrot + - staging + - juliett + skip_cache: + description: Skip build cache + required: false + type: boolean + default: false + push_docker_image: + description: Build and push the Docker image + required: false + type: boolean + default: false + workflow_call: + inputs: + target_environment: + required: false + type: string + default: foxtrot + skip_cache: + required: false + type: boolean + default: false + push_docker_image: + required: false + type: boolean + default: false + ref: + required: false + type: string + default: '' + secrets: + E2B_PROD_API_KEY: + required: false + E2B_STAGING_API_KEY: + required: false + E2B_JULIETT_API_KEY: + required: false + DOCKERHUB_USERNAME: + required: false + DOCKERHUB_TOKEN: + required: false + +concurrency: + group: Release-${{ github.ref }}-${{ inputs.target_environment }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + build-docker-image: + name: Build Docker Image + if: ${{ inputs.push_docker_image }} + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + ref: ${{ inputs.ref }} + + - name: Parse .tool-versions + uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1 + with: + filename: '.tool-versions' + uppercase: 'true' + prefix: 'tool_version_' + + - name: Set package version + working-directory: ./packages/code-interpreter-template + run: | + VERSION=$(cat ../charts-python/pyproject.toml | grep version | cut -d '"' -f 2) + echo "Version: $VERSION" + sed -i "s/e2b_charts/e2b_charts==${VERSION}/g" requirements.txt + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Log in to DockerHub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Install uv + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + with: + version: '${{ env.TOOL_VERSION_UV }}' + python-version: '${{ env.TOOL_VERSION_PYTHON }}' + activate-environment: true + + - name: Install development dependencies + working-directory: ./packages/code-interpreter-template + run: uv pip install -r requirements-dev.txt + + - name: Build and push to DockerHub + working-directory: ./packages/code-interpreter-template + run: | + python build_docker.py | docker buildx build \ + --platform linux/amd64 \ + --push \ + --tag ${{ secrets.DOCKERHUB_USERNAME }}/code-interpreter:latest -f - . + + build-template: + name: Build E2B template + needs: [build-docker-image] + if: ${{ !cancelled() && !contains(needs.*.result, 'failure') }} + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + ref: ${{ inputs.ref }} + + - name: Parse .tool-versions + uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1 + with: + filename: '.tool-versions' + uppercase: 'true' + prefix: 'tool_version_' + + - name: Set package version + working-directory: ./packages/code-interpreter-template + run: | + VERSION=$(cat ../charts-python/pyproject.toml | grep version | cut -d '"' -f 2) + echo "Version: $VERSION" + sed -i "s/e2b_charts/e2b_charts==${VERSION}/g" requirements.txt + + - name: Install uv + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + with: + version: '${{ env.TOOL_VERSION_UV }}' + python-version: '${{ env.TOOL_VERSION_PYTHON }}' + activate-environment: true + + - name: Install development dependencies + working-directory: ./packages/code-interpreter-template + run: uv pip install -r requirements-dev.txt + + - name: Resolve target environment + env: + TARGET_ENVIRONMENT: ${{ inputs.target_environment }} + FOXTROT_DOMAIN: ${{ vars.E2B_DOMAIN }} + FOXTROT_API_KEY: ${{ secrets.E2B_PROD_API_KEY }} + STAGING_API_KEY: ${{ secrets.E2B_STAGING_API_KEY }} + JULIETT_API_KEY: ${{ secrets.E2B_JULIETT_API_KEY }} + run: | + set -eu + + case "$TARGET_ENVIRONMENT" in + foxtrot) + E2B_DOMAIN="$FOXTROT_DOMAIN" + E2B_API_KEY="$FOXTROT_API_KEY" + ;; + staging) + E2B_DOMAIN="e2b-staging.dev" + E2B_API_KEY="$STAGING_API_KEY" + ;; + juliett) + E2B_DOMAIN="e2b-juliett.dev" + E2B_API_KEY="$JULIETT_API_KEY" + ;; + *) + echo "Unknown target environment: $TARGET_ENVIRONMENT" >&2 + exit 1 + ;; + esac + + if [ -z "$E2B_DOMAIN" ]; then + echo "Missing E2B domain for target environment: $TARGET_ENVIRONMENT" >&2 + exit 1 + fi + + if [ -z "$E2B_API_KEY" ]; then + echo "Missing API key secret for target environment: $TARGET_ENVIRONMENT" >&2 + exit 1 + fi + + echo "::add-mask::$E2B_API_KEY" + + { + echo "E2B_DOMAIN=$E2B_DOMAIN" + echo "E2B_API_KEY=$E2B_API_KEY" + } >> "$GITHUB_ENV" + + { + echo "### Build target" + echo + echo "Target: $TARGET_ENVIRONMENT" + echo "Domain: $E2B_DOMAIN" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Build E2B template + id: build-template + working-directory: ./packages/code-interpreter-template + run: python build_prod.py + env: + SKIP_CACHE: ${{ inputs.skip_cache }} diff --git a/.github/workflows/code_interpreter_template_tests.yml b/.github/workflows/code_interpreter_template_tests.yml new file mode 100644 index 0000000000..b3781b27fc --- /dev/null +++ b/.github/workflows/code_interpreter_template_tests.yml @@ -0,0 +1,133 @@ +name: Test Code Interpreter Template + +on: + workflow_call: + inputs: + E2B_DOMAIN: + required: false + type: string + default: '' + run_recovery_tests: + required: false + type: boolean + default: true + secrets: + E2B_API_KEY: + required: true + outputs: + template_id: + description: The ID of the built template + value: ${{ jobs.build.outputs.template_id }} + +permissions: + contents: read + +jobs: + build: + name: Build E2B Template + runs-on: ubuntu-latest + outputs: + template_id: ${{ steps.build-template.outputs.template_id }} + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Parse .tool-versions + uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1 + with: + filename: '.tool-versions' + uppercase: 'true' + prefix: 'tool_version_' + + - name: Set package version + working-directory: ./packages/code-interpreter-template + run: | + VERSION=$(cat ../charts-python/pyproject.toml | grep version | cut -d '"' -f 2) + echo "Version: $VERSION" + sed -i "s/e2b_charts/e2b_charts==${VERSION}/g" requirements.txt + + - name: Install uv + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + with: + version: '${{ env.TOOL_VERSION_UV }}' + python-version: '${{ env.TOOL_VERSION_PYTHON }}' + activate-environment: true + + - name: Install development dependencies + working-directory: ./packages/code-interpreter-template + run: uv pip install -r requirements-dev.txt + + - name: Generate Template Name + id: generate-template-id + run: | + E2B_TESTS_TEMPLATE=e2b-code-interpreter-ci-$(uuidgen) + echo "Generated Template Name: $E2B_TESTS_TEMPLATE" + echo "template_name=$E2B_TESTS_TEMPLATE" >> "$GITHUB_OUTPUT" + + - name: Build E2B template + id: build-template + working-directory: ./packages/code-interpreter-template + run: python build_ci.py + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }} + E2B_TESTS_TEMPLATE: ${{ steps.generate-template-id.outputs.template_name }} + + test: + needs: [build] + defaults: + run: + working-directory: ./packages/code-interpreter-template/tests + shell: bash + name: Code Interpreter - HTTP tests + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Parse .tool-versions + uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1 + with: + filename: '.tool-versions' + uppercase: 'true' + prefix: 'tool_version_' + + - name: Install uv + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + with: + version: '${{ env.TOOL_VERSION_UV }}' + python-version: '${{ env.TOOL_VERSION_PYTHON }}' + enable-cache: true + + - name: Install dependencies + run: uv sync --locked + + - name: Run tests + run: | + if [[ "${{ inputs.run_recovery_tests }}" == "true" ]]; then + uv run pytest --verbose --numprocesses=1 + else + uv run pytest --verbose --numprocesses=1 \ + --ignore=test_interrupt.py \ + --ignore=test_systemd.py \ + --deselect=test_async.py::test_async_interrupt + fi + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }} + E2B_TESTS_TEMPLATE: ${{ needs.build.outputs.template_id }} + + cleanup: + needs: [build, test] + if: always() && needs.build.result == 'success' + name: Cleanup Build Template + runs-on: ubuntu-latest + steps: + - name: Install E2B CLI + run: npm install -g @e2b/cli + + - name: Cleanup E2B template + run: e2b template delete -y "${{ needs.build.outputs.template_id }}" + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }} diff --git a/.github/workflows/publish_packages.yml b/.github/workflows/publish_packages.yml index 3f8f9d9f9a..56a3ce6dfc 100644 --- a/.github/workflows/publish_packages.yml +++ b/.github/workflows/publish_packages.yml @@ -7,6 +7,10 @@ on: required: true VERSION_BUMPER_SECRET: required: true + outputs: + released_sha: + description: The commit containing the published package versions + value: ${{ jobs.test.outputs.released_sha }} permissions: contents: write @@ -16,6 +20,8 @@ jobs: test: name: Build and test SDK runs-on: ubuntu-22.04 + outputs: + released_sha: ${{ steps.push.outputs.sha }} # Declared here rather than on the `publish` job in release.yml: a job that # `uses:` a reusable workflow cannot carry `environment`. The environment's # deployment branch policy only allows main. @@ -110,6 +116,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Push new versions + id: push # A failed publish may have uploaded some Python packages before tagging. # Preserve the versions after any attempt so a later release cannot reuse # them for different contents. @@ -122,5 +129,6 @@ jobs: git merge --no-edit FETCH_HEAD git push fi + echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3fef4a5d34..d831748116 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,8 @@ jobs: desktop_python: ${{ steps.desktop_python.outputs.release }} dockerfile_utils_js: ${{ steps.dockerfile_utils_js.outputs.release }} dockerfile_utils_python: ${{ steps.dockerfile_utils_python.outputs.release }} + code_interpreter_template: ${{ steps.code_interpreter_template.outputs.release }} + charts_python: ${{ steps.charts_python.outputs.release }} itinerary: ${{ steps.itinerary.outputs.itinerary }} steps: - name: Check the ref @@ -138,6 +140,20 @@ jobs: IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/dockerfile-utils-python") echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT" + - name: Check Code Interpreter Template Release + id: code_interpreter_template + if: steps.version.outputs.release == 'true' + run: | + IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/code-interpreter-template") + echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT" + + - name: Check Charts Python Release + id: charts_python + if: steps.version.outputs.release == 'true' + run: | + IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/charts-python") + echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT" + - name: Build release itinerary id: itinerary if: steps.version.outputs.release == 'true' @@ -215,6 +231,19 @@ jobs: if: needs.preflight.outputs.dockerfile_utils_python == 'true' uses: ./.github/workflows/dockerfile_utils_python_tests.yml + code-interpreter-template-tests: + name: Code Interpreter Template Tests + needs: [preflight] + if: needs.preflight.outputs.code_interpreter_template == 'true' + uses: ./.github/workflows/code_interpreter_template_tests.yml + secrets: inherit + + charts-python-tests: + name: Charts Python Tests + needs: [preflight] + if: needs.preflight.outputs.charts_python == 'true' + uses: ./.github/workflows/charts_python_tests.yml + publish: name: Publish needs: @@ -228,10 +257,23 @@ jobs: - desktop-python-tests - dockerfile-utils-js-tests - dockerfile-utils-python-tests + - code-interpreter-template-tests + - charts-python-tests if: (!cancelled()) && !contains(needs.*.result, 'failure') && needs.preflight.outputs.release == 'true' uses: ./.github/workflows/publish_packages.yml secrets: inherit + code-interpreter-template-build: + name: Build Code Interpreter Template + needs: [preflight, publish] + if: (!cancelled()) && needs.publish.result == 'success' && (needs.preflight.outputs.code_interpreter_template == 'true' || needs.preflight.outputs.charts_python == 'true') + uses: ./.github/workflows/code_interpreter_template_build.yml + with: + target_environment: foxtrot + push_docker_image: true + ref: ${{ needs.publish.outputs.released_sha }} + secrets: inherit + report-start: needs: [preflight] if: needs.preflight.outputs.release == 'true' @@ -265,7 +307,10 @@ jobs: - desktop-python-tests - dockerfile-utils-js-tests - dockerfile-utils-python-tests + - code-interpreter-template-tests + - charts-python-tests - publish + - code-interpreter-template-build if: failure() name: Release Failed - Slack Notification runs-on: ubuntu-latest diff --git a/.github/workflows/sdk_tests.yml b/.github/workflows/sdk_tests.yml index 4aeaea69a1..6b3e162a9f 100644 --- a/.github/workflows/sdk_tests.yml +++ b/.github/workflows/sdk_tests.yml @@ -31,6 +31,8 @@ jobs: cli: ${{ steps.filter.outputs.cli }} code_interpreter_js: ${{ steps.filter.outputs.code_interpreter_js }} code_interpreter_python: ${{ steps.filter.outputs.code_interpreter_python }} + code_interpreter_template: ${{ steps.filter.outputs.code_interpreter_template }} + charts_python: ${{ steps.filter.outputs.charts_python }} desktop_js: ${{ steps.filter.outputs.desktop_js }} desktop_python: ${{ steps.filter.outputs.desktop_python }} dockerfile_utils_js: ${{ steps.filter.outputs.dockerfile_utils_js }} @@ -86,6 +88,17 @@ jobs: - 'packages/code-interpreter-python/**/!(*.md)' - 'packages/python-sdk/**/!(*.md)' - 'packages/dockerfile-utils-python/**/!(*.md)' + code_interpreter_template: + - '.github/workflows/sdk_tests.yml' + - '.github/workflows/code_interpreter_template_tests.yml' + - '.tool-versions' + - 'packages/code-interpreter-template/**/!(*.md)' + - 'packages/charts-python/pyproject.toml' + charts_python: + - '.github/workflows/sdk_tests.yml' + - '.github/workflows/charts_python_tests.yml' + - '.tool-versions' + - 'packages/charts-python/**/!(*.md)' desktop_js: - *shared - '.github/workflows/desktop_js_tests.yml' @@ -152,6 +165,20 @@ jobs: secrets: E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + code-interpreter-template-tests: + name: Production / Code Interpreter Template Tests + needs: changes + if: ${{ needs.changes.outputs.code_interpreter_template == 'true' || github.event_name == 'workflow_dispatch' }} + uses: ./.github/workflows/code_interpreter_template_tests.yml + secrets: + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + + charts-python-tests: + name: Charts Python Tests + needs: changes + if: ${{ needs.changes.outputs.charts_python == 'true' || github.event_name == 'workflow_dispatch' }} + uses: ./.github/workflows/charts_python_tests.yml + desktop-js-tests: name: Production / Desktop JS SDK Tests needs: changes @@ -271,6 +298,8 @@ jobs: - cli-tests - code-interpreter-js-tests - code-interpreter-python-tests + - code-interpreter-template-tests + - charts-python-tests - desktop-js-tests - desktop-python-tests - dockerfile-utils-js-tests diff --git a/README.md b/README.md index a7a85ff0a2..037555a912 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ npm i @e2b/code-interpreter # JavaScript/TypeScript pip install e2b-code-interpreter # Python ``` +The [`@e2b/code-interpreter-template`](./packages/code-interpreter-template) +and [`@e2b/charts-python`](./packages/charts-python) packages provide the +Code Interpreter sandbox template and Python chart extraction package. + ```ts import { Sandbox } from '@e2b/code-interpreter' diff --git a/packages/charts-python/LICENSE b/packages/charts-python/LICENSE new file mode 100644 index 0000000000..4aa6529d10 --- /dev/null +++ b/packages/charts-python/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 FOUNDRYLABS, INC. + +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. diff --git a/packages/charts-python/README.md b/packages/charts-python/README.md new file mode 100644 index 0000000000..7c23e47f5e --- /dev/null +++ b/packages/charts-python/README.md @@ -0,0 +1,3 @@ +# Extracting Data for Code Interpreter SDK + +This package is a utility used to extract data in the Code Interpreter SDK from, e.g., DataFrames and matplotlib plots. diff --git a/packages/charts-python/e2b_charts/__init__.py b/packages/charts-python/e2b_charts/__init__.py new file mode 100644 index 0000000000..adb4954c69 --- /dev/null +++ b/packages/charts-python/e2b_charts/__init__.py @@ -0,0 +1 @@ +from .main import chart_figure_to_chart, chart_figure_to_dict diff --git a/packages/charts-python/e2b_charts/charts/__init__.py b/packages/charts-python/e2b_charts/charts/__init__.py new file mode 100644 index 0000000000..0cfbebd32f --- /dev/null +++ b/packages/charts-python/e2b_charts/charts/__init__.py @@ -0,0 +1,4 @@ +from .base import ChartType, Chart +from .bars import BarChart, BoxAndWhiskerChart +from .pie import PieChart +from .planar import ScatterChart, LineChart diff --git a/packages/charts-python/e2b_charts/charts/bars.py b/packages/charts-python/e2b_charts/charts/bars.py new file mode 100644 index 0000000000..3d7a943726 --- /dev/null +++ b/packages/charts-python/e2b_charts/charts/bars.py @@ -0,0 +1,140 @@ +from typing import Literal, List + +from matplotlib.axes import Axes +from pydantic import BaseModel, Field + +from .base import Chart2D, ChartType +from ..utils.rounding import dynamic_round + + +class BarData(BaseModel): + label: str + group: str + value: float + + +class BarChart(Chart2D): + type: Literal[ChartType.BAR] = ChartType.BAR + + elements: List[BarData] = Field(default_factory=list) + + def _extract_info(self, ax: Axes) -> None: + super()._extract_info(ax) + for container in ax.containers: + group_label = container.get_label() + if group_label.startswith("_container"): + number = int(group_label[10:]) + group_label = f"Group {number}" + + heights = [rect.get_height() for rect in container] + if all(height == heights[0] for height in heights): + # vertical bars + self._change_orientation() + labels = [label.get_text() for label in ax.get_yticklabels()] + values = [rect.get_width() for rect in container] + else: + # horizontal bars + labels = [label.get_text() for label in ax.get_xticklabels()] + values = heights + for label, value in zip(labels, values): + bar = BarData(label=label, value=value, group=group_label) + self.elements.append(bar) + + +class BoxAndWhiskerData(BaseModel): + label: str + min: float + first_quartile: float + median: float + third_quartile: float + max: float + outliers: List[float] + + +class BoxAndWhiskerChart(Chart2D): + type: Literal[ChartType.BOX_AND_WHISKER] = ChartType.BOX_AND_WHISKER + + elements: List[BoxAndWhiskerData] = Field(default_factory=list) + + def _extract_info(self, ax: Axes) -> None: + super()._extract_info(ax) + labels = [item.get_text() for item in ax.get_xticklabels()] + + boxes = [] + for label, box in zip(labels, ax.patches): + vertices = box.get_path().vertices + x_vertices = [dynamic_round(x) for x in vertices[:, 0]] + y_vertices = [dynamic_round(y) for y in vertices[:, 1]] + x = min(x_vertices) + y = min(y_vertices) + boxes.append( + { + "x": x, + "y": y, + "label": label, + "width": max(x_vertices) - x, + "height": max(y_vertices) - y, + "outliers": [], + } + ) + + orientation = "horizontal" + if all(box["height"] == boxes[0]["height"] for box in boxes): + orientation = "vertical" + + if orientation == "vertical": + self._change_orientation() + for box in boxes: + box["x"], box["y"] = box["y"], box["x"] + box["width"], box["height"] = box["height"], box["width"] + + for i, line in enumerate(ax.lines): + xdata = [dynamic_round(x) for x in line.get_xdata()] + ydata = [dynamic_round(y) for y in line.get_ydata()] + + if orientation == "vertical": + xdata, ydata = ydata, xdata + + if len(xdata) == 1: + for box in boxes: + if box["x"] <= xdata[0] <= box["x"] + box["width"]: + break + else: + continue + + box["outliers"].append(ydata[0]) + if len(ydata) != 2: + continue + for box in boxes: + if box["x"] <= xdata[0] <= xdata[1] <= box["x"] + box["width"]: + break + else: + continue + + if ( + # Check if the line is inside the box, prevent floating point errors + ydata[0] == ydata[1] + and box["y"] <= ydata[0] <= box["y"] + box["height"] + ): + box["median"] = ydata[0] + continue + + lower_value = min(ydata) + upper_value = max(ydata) + if upper_value == box["y"]: + box["whisker_lower"] = lower_value + elif lower_value == box["y"] + box["height"]: + box["whisker_upper"] = upper_value + + self.elements = [ + BoxAndWhiskerData( + label=box["label"], + min=box["whisker_lower"], + first_quartile=box["y"], + median=box["median"], + third_quartile=box["y"] + box["height"], + max=box["whisker_upper"], + outliers=box["outliers"], + ) + for box in boxes + ] diff --git a/packages/charts-python/e2b_charts/charts/base.py b/packages/charts-python/e2b_charts/charts/base.py new file mode 100644 index 0000000000..0985a9d8e5 --- /dev/null +++ b/packages/charts-python/e2b_charts/charts/base.py @@ -0,0 +1,75 @@ +import enum +import re +from typing import Optional, List, Any + +from matplotlib.axes import Axes +from pydantic import BaseModel, Field + + +class ChartType(str, enum.Enum): + LINE = "line" + SCATTER = "scatter" + BAR = "bar" + PIE = "pie" + BOX_AND_WHISKER = "box_and_whisker" + SUPERCHART = "superchart" + UNKNOWN = "unknown" + + +class Chart(BaseModel): + type: ChartType + title: Optional[str] = None + + elements: List[Any] = Field(default_factory=list) + + def __init__(self, ax: Optional[Axes] = None, **kwargs): + super().__init__(**kwargs) + if ax: + self._extract_info(ax) + + def _extract_info(self, ax: Axes) -> None: + """ + Function to extract information for Chart + """ + title = ax.get_title() + if title == "": + title = None + + self.title = title + + +class Chart2D(Chart): + x_label: Optional[str] = None + y_label: Optional[str] = None + x_unit: Optional[str] = None + y_unit: Optional[str] = None + + def _extract_info(self, ax: Axes) -> None: + """ + Function to extract information for Chart2D + """ + super()._extract_info(ax) + x_label = ax.get_xlabel() + if x_label == "": + x_label = None + self.x_label = x_label + + y_label = ax.get_ylabel() + if y_label == "": + y_label = None + self.y_label = y_label + + regex = r"\s\((.*?)\)|\[(.*?)\]" + if self.x_label: + match = re.search(regex, self.x_label) + if match: + self.x_unit = match.group(1) or match.group(2) + + if self.y_label: + match = re.search(regex, self.y_label) + if match: + self.y_unit = match.group(1) or match.group(2) + + def _change_orientation(self): + self.x_label, self.y_label = self.y_label, self.x_label + self.x_unit, self.y_unit = self.y_unit, self.x_unit diff --git a/packages/charts-python/e2b_charts/charts/pie.py b/packages/charts-python/e2b_charts/charts/pie.py new file mode 100644 index 0000000000..2581830be3 --- /dev/null +++ b/packages/charts-python/e2b_charts/charts/pie.py @@ -0,0 +1,36 @@ +from decimal import Decimal +from typing import Literal, List + +from matplotlib.axes import Axes +from pydantic import BaseModel, Field + +from .base import Chart, ChartType +from ..utils.rounding import dynamic_round + + +class PieData(BaseModel): + label: str + angle: float + radius: float + + +class PieChart(Chart): + type: Literal[ChartType.PIE] = ChartType.PIE + + elements: List[PieData] = Field(default_factory=list) + + def _extract_info(self, ax: Axes) -> None: + super()._extract_info(ax) + + for wedge in ax.patches: + pie_data = PieData( + label=wedge.get_label(), + angle=abs( + dynamic_round( + Decimal(float(wedge.theta2)) - Decimal(float(wedge.theta1)) + ) + ), + radius=wedge.r, + ) + + self.elements.append(pie_data) diff --git a/packages/charts-python/e2b_charts/charts/planar.py b/packages/charts-python/e2b_charts/charts/planar.py new file mode 100644 index 0000000000..bb7d025d5f --- /dev/null +++ b/packages/charts-python/e2b_charts/charts/planar.py @@ -0,0 +1,132 @@ +from datetime import date +from typing import List, Tuple, Union, Sequence, Any, Literal + +import matplotlib +import numpy +from matplotlib.axes import Axes +from matplotlib.dates import _SwitchableDateConverter +from pydantic import BaseModel, field_validator, Field + +from .base import Chart2D, ChartType +from ..utils import is_grid_line + + +class PointData(BaseModel): + label: str + points: List[Tuple[Union[str, float], Union[str, float]]] + + @field_validator("points", mode="before") + @classmethod + def transform_points( + cls, value + ) -> List[Tuple[Union[str, float], Union[str, float]]]: + parsed_value = [] + for x, y in value: + x = cls._parse_point(x) + y = cls._parse_point(y) + parsed_value.append((x, y)) + return parsed_value + + @staticmethod + def _parse_point(point): + if isinstance(point, date): + return point.isoformat() + if isinstance(point, numpy.datetime64): + return point.astype("datetime64[s]").astype(str) + return point + + +class PointChart(Chart2D): + x_ticks: List[Union[str, float]] = Field(default_factory=list) + x_tick_labels: List[str] = Field(default_factory=list) + x_scale: str = Field(default="linear") + + y_ticks: List[Union[str, float]] = Field(default_factory=list) + y_tick_labels: List[str] = Field(default_factory=list) + y_scale: str = Field(default="linear") + + elements: List[PointData] = Field(default_factory=list) + + def _extract_info(self, ax: Axes) -> None: + """ + Function to extract information for PointChart + """ + super()._extract_info(ax) + + self.x_tick_labels = [label.get_text() for label in ax.get_xticklabels()] + + x_ticks = ax.get_xticks() + self.x_ticks = self._extract_ticks_info(ax.xaxis.converter, x_ticks) + self.x_scale = self._detect_scale( + ax.xaxis.converter, ax.get_xscale(), self.x_ticks, self.x_tick_labels + ) + + self.y_tick_labels = [label.get_text() for label in ax.get_yticklabels()] + self.y_ticks = self._extract_ticks_info(ax.yaxis.converter, ax.get_yticks()) + self.y_scale = self._detect_scale( + ax.yaxis.converter, ax.get_yscale(), self.y_ticks, self.y_tick_labels + ) + + @staticmethod + def _detect_scale(converter, scale: str, ticks: Sequence, labels: Sequence) -> str: + # If the converter is a date converter, it's a datetime scale + if isinstance(converter, _SwitchableDateConverter): + return "datetime" + + # If the scale is not linear, it can't be categorical + if scale != "linear": + return scale + + # If all the ticks are integers and are in order from 0 to n-1 + # and the labels aren't corresponding to the ticks, it's categorical + for i, tick_and_label in enumerate(zip(ticks, labels)): + tick, label = tick_and_label + if isinstance(tick, (int, float)) and tick == i and str(i) != label: + continue + # Found a tick, which wouldn't be in a categorical scale + return "linear" + + return "categorical" + + @staticmethod + def _extract_ticks_info(converter: Any, ticks: Sequence) -> list: + if isinstance(converter, _SwitchableDateConverter): + return [matplotlib.dates.num2date(tick).isoformat() for tick in ticks] + else: + example_tick = ticks[0] + + if isinstance(example_tick, (int, float)): + return [float(tick) for tick in ticks] + else: + return list(ticks) + + +class LineChart(PointChart): + type: Literal[ChartType.LINE] = ChartType.LINE + + def _extract_info(self, ax: Axes) -> None: + super()._extract_info(ax) + + for line in ax.get_lines(): + if is_grid_line(line): + continue + label = line.get_label() + if label.startswith("_child"): + number = int(label[6:]) + label = f"Line {number}" + + points = [(x, y) for x, y in zip(line.get_xdata(), line.get_ydata())] + line_data = PointData(label=label, points=points) + self.elements.append(line_data) + + +class ScatterChart(PointChart): + type: Literal[ChartType.SCATTER] = ChartType.SCATTER + + def _extract_info(self, ax: Axes) -> None: + super()._extract_info(ax) + + for collection in ax.collections: + points = [(x, y) for x, y in collection.get_offsets()] + scatter_data = PointData(label=collection.get_label(), points=points) + self.elements.append(scatter_data) diff --git a/packages/charts-python/e2b_charts/main.py b/packages/charts-python/e2b_charts/main.py new file mode 100644 index 0000000000..7f51dbbd73 --- /dev/null +++ b/packages/charts-python/e2b_charts/main.py @@ -0,0 +1,119 @@ +from typing import Optional, List, Literal + +from matplotlib.axes import Axes +from matplotlib.collections import PathCollection +from matplotlib.lines import Line2D +from matplotlib.patches import Rectangle, Wedge, PathPatch +from matplotlib.pyplot import Figure + +from matplotlib.text import Text +from pydantic import Field + +from .charts import ( + ChartType, + Chart, + LineChart, + BarChart, + BoxAndWhiskerChart, + PieChart, + ScatterChart, +) +from .utils.filtering import is_grid_line + + +class SuperChart(Chart): + type: Literal[ChartType.SUPERCHART] = ChartType.SUPERCHART + elements: List[ + LineChart | ScatterChart | BarChart | PieChart | BoxAndWhiskerChart + ] = Field(default_factory=list) + + def __init__(self, figure: Figure): + title = figure.get_suptitle() + super().__init__(title=title) + + self.elements = [get_chart_from_ax(ax) for ax in figure.axes] + + +def _get_type_of_chart(ax: Axes) -> ChartType: + objects = list(filter(lambda obj: not isinstance(obj, Text), ax._children)) + + # Check for Line plots + if all(isinstance(line, Line2D) for line in objects): + return ChartType.LINE + + if all(isinstance(box_or_path, (PathPatch, Line2D)) for box_or_path in objects): + return ChartType.BOX_AND_WHISKER + + filtered = [] + for obj in objects: + if isinstance(obj, Line2D) and is_grid_line(obj): + continue + filtered.append(obj) + + objects = filtered + + # Check for Scatter plots + if all(isinstance(path, PathCollection) for path in objects): + return ChartType.SCATTER + + # Check for Pie plots + if all(isinstance(artist, Wedge) for artist in objects): + return ChartType.PIE + + # Check for Bar plots + if all(isinstance(rect, Rectangle) for rect in objects): + return ChartType.BAR + + return ChartType.UNKNOWN + + +def get_chart_from_ax( + ax: Axes, +) -> LineChart | ScatterChart | BarChart | PieChart | BoxAndWhiskerChart | Chart: + chart_type = _get_type_of_chart(ax) + + if chart_type == ChartType.LINE: + chart = LineChart(ax=ax) + elif chart_type == ChartType.SCATTER: + chart = ScatterChart(ax=ax) + elif chart_type == ChartType.BAR: + chart = BarChart(ax=ax) + elif chart_type == ChartType.PIE: + chart = PieChart(ax=ax) + elif chart_type == ChartType.BOX_AND_WHISKER: + chart = BoxAndWhiskerChart(ax=ax) + else: + chart = Chart(ax=ax, type=chart_type) + + return chart + + +def is_figure_blank(axes: List[Axes]) -> bool: + """Check if a Matplotlib figure is blank (has no user-added artists).""" + for ax in axes: + if ax.has_data(): + return False # The figure contains user-added data + return True # No data found, figure is blank + + +def chart_figure_to_chart(figure: Figure) -> Optional[Chart]: + """ + This method is used to extract data from the figure object to a dictionary + """ + # Get all Axes objects from the Figure + axes = figure.get_axes() + + if not axes or is_figure_blank(axes): + return + elif len(axes) > 1: + return SuperChart(figure=figure) + else: + ax = axes[0] + return get_chart_from_ax(ax) + + +def chart_figure_to_dict(figure: Figure) -> dict: + chart = chart_figure_to_chart(figure) + if chart: + return chart.model_dump() + return {} diff --git a/packages/charts-python/e2b_charts/utils/__init__.py b/packages/charts-python/e2b_charts/utils/__init__.py new file mode 100644 index 0000000000..7822a91f85 --- /dev/null +++ b/packages/charts-python/e2b_charts/utils/__init__.py @@ -0,0 +1 @@ +from .filtering import is_grid_line diff --git a/packages/charts-python/e2b_charts/utils/filtering.py b/packages/charts-python/e2b_charts/utils/filtering.py new file mode 100644 index 0000000000..8394c27ba2 --- /dev/null +++ b/packages/charts-python/e2b_charts/utils/filtering.py @@ -0,0 +1,16 @@ +from matplotlib.lines import Line2D + + +def is_grid_line(line: Line2D) -> bool: + x_data = line.get_xdata() + if len(x_data) != 2: + return False + + y_data = line.get_ydata() + if len(y_data) != 2: + return False + + if x_data[0] == x_data[1] or y_data[0] == y_data[1]: + return True + + return False diff --git a/packages/charts-python/e2b_charts/utils/rounding.py b/packages/charts-python/e2b_charts/utils/rounding.py new file mode 100644 index 0000000000..4efbb66719 --- /dev/null +++ b/packages/charts-python/e2b_charts/utils/rounding.py @@ -0,0 +1,13 @@ +from decimal import Decimal, localcontext + + +def dynamic_round(number): + # Convert to Decimal for precise control + decimal_number = Decimal(str(number)) + + # Dynamically determine precision based on magnitude + precision = max(1, 8 - decimal_number.adjusted()) # 8 digits of precision + + with localcontext() as ctx: + ctx.prec = precision # Set the dynamic precision + return +decimal_number # The + operator applies rounding diff --git a/packages/charts-python/package.json b/packages/charts-python/package.json new file mode 100644 index 0000000000..17e6cd906d --- /dev/null +++ b/packages/charts-python/package.json @@ -0,0 +1,13 @@ +{ + "name": "@e2b/charts-python", + "private": true, + "version": "1.0.0", + "scripts": { + "test": "uv run pytest --verbose -x", + "postVersion": "uv version --frozen $(pnpm pkg get version --workspaces=false | tr -d \\\")", + "lock": "uv lock", + "postPublish": "uv build && uv publish --trusted-publishing always --check-url https://pypi.org/simple/", + "lint": "uv run ruff check .", + "format": "uv run ruff format ." + } +} diff --git a/packages/charts-python/pyproject.toml b/packages/charts-python/pyproject.toml new file mode 100644 index 0000000000..e20b9c32e8 --- /dev/null +++ b/packages/charts-python/pyproject.toml @@ -0,0 +1,38 @@ +[project] +name = "e2b-charts" +version = "1.0.0" +description = "Package for extracting data for E2B Code Interpreter" +authors = [{ name = "e2b", email = "hello@e2b.dev" }] +requires-python = ">=3.10" +readme = "README.md" +license = "MIT" +license-files = ["LICENSE"] +dependencies = [ + "numpy>=2.2.6,<3", + "matplotlib>=3.10.3,<4", + "pydantic>=2.9.1,<3", +] + +[project.urls] +Homepage = "https://e2b.dev/" +Repository = "https://github.com/e2b-dev/E2B/tree/main/packages/charts-python" +"Bug Tracker" = "https://github.com/e2b-dev/E2B/issues" + +[dependency-groups] +dev = [ + "pytest>=9.0.3,<10", + "python-dotenv>=1.2.1,<2", + "pytest-dotenv>=0.5.2,<0.6", + "ruff>=0.11.12,<0.12", +] + +[tool.uv.build-backend] +module-name = ["e2b_charts"] +module-root = "" + +[build-system] +requires = ["uv_build>=0.10.0,<0.11.0"] +build-backend = "uv_build" + +[tool.ruff.lint] +ignore = ["F401", "F403"] diff --git a/packages/charts-python/tests/charts/test_bar.py b/packages/charts-python/tests/charts/test_bar.py new file mode 100644 index 0000000000..246b4d6252 --- /dev/null +++ b/packages/charts-python/tests/charts/test_bar.py @@ -0,0 +1,49 @@ +import matplotlib.pyplot as plt + +from e2b_charts import chart_figure_to_chart +from e2b_charts.charts import BarChart, ChartType + + +def _prep_chart_figure(): + # Prepare data + authors = ["Author A", "Author B", "Author C", "Author D"] + sales = [100, 200, 300, 400] + + # Create and customize the bar chart + plt.figure(figsize=(10, 6)) + plt.bar(authors, sales, label="Books Sold", color="blue") + plt.xlabel("Authors") + plt.ylabel("Number of Books Sold") + plt.title("Book Sales by Authors") + + # Display the chart + plt.tight_layout() + return plt.gcf() + + +def test_chart_bar(): + figure = _prep_chart_figure() + chart = chart_figure_to_chart(figure) + assert chart + + assert isinstance(chart, BarChart) + assert chart.type == ChartType.BAR + assert chart.title == "Book Sales by Authors" + + assert chart.x_label == "Authors" + assert chart.y_label == "Number of Books Sold" + + assert chart.x_unit is None + assert chart.y_unit is None + + bars = chart.elements + assert len(bars) == 4 + + assert [bar.value for bar in bars] == [100, 200, 300, 400] + assert [bar.label for bar in bars] == [ + "Author A", + "Author B", + "Author C", + "Author D", + ] + assert [bar.group for bar in bars] == ["Books Sold"] * 4 diff --git a/packages/charts-python/tests/charts/test_blank.py b/packages/charts-python/tests/charts/test_blank.py new file mode 100644 index 0000000000..7fb95c43dc --- /dev/null +++ b/packages/charts-python/tests/charts/test_blank.py @@ -0,0 +1,9 @@ +import matplotlib.pyplot as plt + +from e2b_charts import chart_figure_to_chart + + +def test_blank_chart(): + figure, _ = plt.subplots() + chart = chart_figure_to_chart(figure) + assert chart is None diff --git a/packages/charts-python/tests/charts/test_box_and_whiskers.py b/packages/charts-python/tests/charts/test_box_and_whiskers.py new file mode 100644 index 0000000000..799e3ffdb9 --- /dev/null +++ b/packages/charts-python/tests/charts/test_box_and_whiskers.py @@ -0,0 +1,57 @@ +import matplotlib.pyplot as plt + +from e2b_charts import chart_figure_to_chart +from e2b_charts.charts import BoxAndWhiskerChart, ChartType + + +def _prep_chart_figure(): + # Sample data + data = { + "Class A": [85, 90, 78, 92, 88], + "Class B": [95, 89, 76, 91, 84, 87], + "Class C": [75, 82, 88, 79, 86], + } + + # Create figure and axis + fig, ax = plt.subplots(figsize=(10, 6)) + + # Plot box plot + ax.boxplot(data.values(), tick_labels=data.keys()) + + # Customize plot + ax.set_title("Exam Scores Distribution") + ax.set_xlabel("Class") + ax.set_ylabel("Score") + + # Set custom colors + ax.boxplot(data.values(), tick_labels=data.keys(), patch_artist=True) + + # Adjust layout and show plot + plt.tight_layout() + return plt.gcf() + + +def test_box_and_whiskers(): + figure = _prep_chart_figure() + chart = chart_figure_to_chart(figure) + assert chart + + assert isinstance(chart, BoxAndWhiskerChart) + assert chart.type == ChartType.BOX_AND_WHISKER + assert chart.title == "Exam Scores Distribution" + + assert chart.x_label == "Class" + assert chart.y_label == "Score" + + assert chart.x_unit is None + assert chart.y_unit is None + + bars = chart.elements + assert len(bars) == 3 + + assert all(isinstance(bar.min, float) for bar in bars) + assert all(isinstance(bar.first_quartile, float) for bar in bars) + assert all(isinstance(bar.median, float) for bar in bars) + assert all(isinstance(bar.third_quartile, float) for bar in bars) + assert all(isinstance(bar.max, float) for bar in bars) + assert all(isinstance(bar.label, str) for bar in bars) diff --git a/packages/charts-python/tests/charts/test_categorical_scale.py b/packages/charts-python/tests/charts/test_categorical_scale.py new file mode 100644 index 0000000000..12d87d23b4 --- /dev/null +++ b/packages/charts-python/tests/charts/test_categorical_scale.py @@ -0,0 +1,25 @@ +import matplotlib.pyplot as plt + +from e2b_charts import chart_figure_to_chart +from e2b_charts.charts import LineChart + + +def _prep_chart_figure(): + x = [1, 2, 3, 4, 5] + y = ["A", "B", "C", "D", "E"] + + # Create the plot + plt.figure(figsize=(10, 6)) + plt.plot(x, y) + + return plt.gcf() + + +def test_categorical_scale(): + figure = _prep_chart_figure() + chart = chart_figure_to_chart(figure) + assert chart + + assert isinstance(chart, LineChart) + assert chart.x_scale == "linear" + assert chart.y_scale == "categorical" diff --git a/packages/charts-python/tests/charts/test_datetime_scale.py b/packages/charts-python/tests/charts/test_datetime_scale.py new file mode 100644 index 0000000000..a851d16b98 --- /dev/null +++ b/packages/charts-python/tests/charts/test_datetime_scale.py @@ -0,0 +1,30 @@ +import numpy as np +import matplotlib.pyplot as plt +import datetime + +from e2b_charts import chart_figure_to_chart +from e2b_charts.charts import LineChart + + +def _prep_chart_figure(): + # Generate x values + dates = [ + datetime.date(2023, 9, 1) + datetime.timedelta(seconds=i) for i in range(100) + ] + y_sin = np.sin(np.linspace(0, 2 * np.pi, 100)) + + # Create the plot + plt.figure(figsize=(10, 6)) + plt.plot(dates, y_sin, label="sin(x)") + + return plt.gcf() + + +def test_datetime_scale(): + figure = _prep_chart_figure() + chart = chart_figure_to_chart(figure) + assert chart + + assert isinstance(chart, LineChart) + assert chart.x_scale == "datetime" + assert chart.y_scale == "linear" diff --git a/packages/charts-python/tests/charts/test_line.py b/packages/charts-python/tests/charts/test_line.py new file mode 100644 index 0000000000..ed58e655ed --- /dev/null +++ b/packages/charts-python/tests/charts/test_line.py @@ -0,0 +1,74 @@ +import numpy as np +import matplotlib.pyplot as plt +import datetime + +from e2b_charts import chart_figure_to_chart +from e2b_charts.charts import LineChart + + +def _prep_chart_figure(): + # Generate x values + dates = [ + datetime.date(2023, 9, 1) + datetime.timedelta(seconds=i) for i in range(100) + ] + + x = np.linspace(0, 2 * np.pi, 100) + # Calculate y values + y_sin = np.sin(x) + y_cos = np.cos(x) + + # Create the plot + plt.figure(figsize=(10, 6)) + plt.plot(dates, y_sin, label="sin(x)") + plt.plot(dates, y_cos, label="cos(x)") + + # Add labels and title + plt.xlabel("Time (s)") + plt.ylabel("Amplitude (Hz)") + plt.title("Plot of sin(x) and cos(x)") + + return plt.gcf() + + +def test_line_chart(): + figure = _prep_chart_figure() + chart = chart_figure_to_chart(figure) + assert chart + + assert isinstance(chart, LineChart) + assert chart.title == "Plot of sin(x) and cos(x)" + + assert chart.x_label == "Time (s)" + assert chart.y_label == "Amplitude (Hz)" + + assert chart.x_unit == "s" + assert chart.y_unit == "Hz" + + assert chart.x_scale == "datetime" + assert chart.y_scale == "linear" + + assert all(isinstance(x, str) for x in chart.x_ticks) + parsed_date = datetime.datetime.fromisoformat(chart.x_ticks[0]) + assert isinstance(parsed_date, datetime.datetime) + assert all(isinstance(y, float) for y in chart.y_ticks) + + assert all(isinstance(x, str) for x in chart.y_tick_labels) + assert all(isinstance(y, str) for y in chart.y_tick_labels) + + lines = chart.elements + assert len(lines) == 2 + + first_line = lines[0] + assert first_line.label == "sin(x)" + assert len(first_line.points) == 100 + assert all(isinstance(point, tuple) for point in first_line.points) + assert all( + isinstance(x, str) and isinstance(y, float) for x, y in first_line.points + ) + + parsed_date = datetime.datetime.fromisoformat(first_line.points[0][0]) + assert isinstance(parsed_date, datetime.datetime) + + second_line = lines[1] + assert second_line.label == "cos(x)" + assert len(second_line.points) == 100 diff --git a/packages/charts-python/tests/charts/test_log_graph.py b/packages/charts-python/tests/charts/test_log_graph.py new file mode 100644 index 0000000000..b15cb72b98 --- /dev/null +++ b/packages/charts-python/tests/charts/test_log_graph.py @@ -0,0 +1,63 @@ +import numpy as np +import matplotlib.pyplot as plt + +from e2b_charts import chart_figure_to_chart +from e2b_charts.charts import LineChart + + +def _prep_chart_figure(): + # Generate x values + x = np.linspace(0, 100, 100) + # Calculate y values + y = np.exp(x) + + # Create the plot + plt.figure(figsize=(10, 6)) + plt.plot(x, y, label="y = e^x") + + # Set log scale for the y-axis + plt.yscale("log") + + # Add labels and title + plt.xlabel("X-axis") + plt.ylabel("Y-axis (log scale)") + plt.title("Chart with Log Scale on Y-axis") + + plt.legend() + plt.grid(True) + + return plt.gcf() + + +def test_log_chart(): + figure = _prep_chart_figure() + chart = chart_figure_to_chart(figure) + assert chart + + assert isinstance(chart, LineChart) + assert chart.title == "Chart with Log Scale on Y-axis" + + assert chart.x_label == "X-axis" + assert chart.y_label == "Y-axis (log scale)" + + assert chart.x_unit is None + assert chart.y_unit == "log scale" + + assert chart.x_scale == "linear" + assert chart.y_scale == "log" + + assert all(isinstance(x, float) for x in chart.x_ticks) + assert all(isinstance(y, float) for y in chart.y_ticks) + + assert all(isinstance(x, str) for x in chart.x_tick_labels) + assert all(isinstance(y, str) for y in chart.y_tick_labels) + + lines = chart.elements + assert len(lines) == 1 + + line = lines[0] + assert line.label == "y = e^x" + assert len(line.points) == 100 + + assert all(isinstance(x, tuple) for x in line.points) + assert all(isinstance(x, float) and isinstance(y, float) for x, y in line.points) diff --git a/packages/charts-python/tests/charts/test_pie.py b/packages/charts-python/tests/charts/test_pie.py new file mode 100644 index 0000000000..28e7ac9992 --- /dev/null +++ b/packages/charts-python/tests/charts/test_pie.py @@ -0,0 +1,53 @@ +import matplotlib.pyplot as plt + +from e2b_charts import chart_figure_to_chart +from e2b_charts.charts import PieChart + + +def _prep_chart_figure(): + # Step 1: Define the data for the pie chart + categories = ["No", "No, in blue"] + sizes = [90, 10] + + # Step 2: Create the figure and axis objects + fig, ax = plt.subplots(figsize=(8, 8)) + + plt.xlabel("x") + plt.ylabel("y") + + # Step 3: Create the pie chart + ax.pie( + sizes, + labels=categories, + autopct="%1.1f%%", + startangle=90, + colors=plt.cm.Pastel1.colors[: len(categories)], + ) + + # Step 4: Add title and legend + ax.axis("equal") # Equal aspect ratio ensures that pie is drawn as a circle + plt.title("Will I wake up early tomorrow?") + + return plt.gcf() + + +def test_pie_chart(): + figure = _prep_chart_figure() + chart = chart_figure_to_chart(figure) + assert chart + + assert isinstance(chart, PieChart) + + assert chart.title == "Will I wake up early tomorrow?" + + assert len(chart.elements) == 2 + + first_data = chart.elements[0] + assert first_data.label == "No" + assert first_data.angle == 324 + assert first_data.radius == 1 + + second_data = chart.elements[1] + assert second_data.label == "No, in blue" + assert second_data.angle == 36 + assert second_data.radius == 1 diff --git a/packages/charts-python/tests/charts/test_scatter.py b/packages/charts-python/tests/charts/test_scatter.py new file mode 100644 index 0000000000..e57582f15b --- /dev/null +++ b/packages/charts-python/tests/charts/test_scatter.py @@ -0,0 +1,58 @@ +import matplotlib.pyplot as plt +import numpy as np + +from e2b_charts import chart_figure_to_chart +from e2b_charts.charts import ScatterChart + + +def _prep_chart_figure(): + # Create data + N = 5 + x1 = np.random.rand(N) + y1 = np.random.rand(N) + x2 = np.random.rand(2 * N) + y2 = np.random.rand(2 * N) + + plt.figure(figsize=(10, 6)) + + plt.xlabel("A") + plt.ylabel("B") + + plt.scatter(x1, y1, c="blue", label="Dataset 1") + plt.scatter(x2, y2, c="red", label="Dataset 2") + + return plt.gcf() + + +def test_scatter_chart(): + figure = _prep_chart_figure() + chart = chart_figure_to_chart(figure) + assert chart + + assert isinstance(chart, ScatterChart) + + assert chart.title is None + assert chart.x_label == "A" + assert chart.y_label == "B" + + assert chart.x_scale == "linear" + assert chart.y_scale == "linear" + + assert all(isinstance(x, float) for x in chart.x_ticks) + assert all(isinstance(y, float) for y in chart.y_ticks) + + assert all(isinstance(x, str) for x in chart.y_tick_labels) + assert all(isinstance(y, str) for y in chart.y_tick_labels) + + assert len(chart.elements) == 2 + + first_data = chart.elements[0] + assert first_data.label == "Dataset 1" + assert len(first_data.points) == 5 + print(first_data.points) + assert all(isinstance(x, tuple) for x in first_data.points) + + second_data = chart.elements[1] + assert second_data.label == "Dataset 2" + assert len(second_data.points) == 10 + assert all(isinstance(x, tuple) for x in second_data.points) diff --git a/packages/charts-python/tests/charts/test_supergraph.py b/packages/charts-python/tests/charts/test_supergraph.py new file mode 100644 index 0000000000..ca3e569e9c --- /dev/null +++ b/packages/charts-python/tests/charts/test_supergraph.py @@ -0,0 +1,66 @@ +import matplotlib.pyplot as plt +import numpy as np + +from e2b_charts import chart_figure_to_chart +from e2b_charts.charts import ( + ChartType, + LineChart, + ScatterChart, +) +from e2b_charts.main import SuperChart + + +def _prep_chart_figure(): + # Data for plotting + x1 = np.linspace(0, 10, 100) + y1 = np.sin(x1) + + # Create a figure with multiple subplots + fig, axs = plt.subplots(1, 2, figsize=(10, 8)) + fig.suptitle("Multiple Charts Example", fontsize=16) + + # Plotting on the different axes + axs[0].plot(x1, y1, "r") + axs[0].set_title("Sine Wave") + axs[0].grid(True) + + N = 5 + x2 = np.random.rand(N) + y2 = np.random.rand(N) + + axs[1].scatter(x2, y2, c="blue", label="Dataset 1") + axs[1].set_xlabel("X") + axs[1].set_ylabel("Y") + axs[1].set_title("Scatter Plot") + axs[1].grid(True) + + return plt.gcf() + + +def test_super_chart(): + figure = _prep_chart_figure() + chart = chart_figure_to_chart(figure) + assert chart + + assert isinstance(chart, SuperChart) + assert chart.type == ChartType.SUPERCHART + assert chart.title == "Multiple Charts Example" + + charts = chart.elements + assert len(charts) == 2 + + first_chart = charts[0] + assert first_chart.title == "Sine Wave" + assert isinstance(first_chart, LineChart) + assert first_chart.x_label is None + assert first_chart.y_label is None + assert len(first_chart.elements) == 1 + assert len(first_chart.elements[0].points) == 100 + + second_chart = charts[1] + assert second_chart.title == "Scatter Plot" + assert isinstance(second_chart, ScatterChart) + assert second_chart.x_label == "X" + assert second_chart.y_label == "Y" + assert len(second_chart.elements) == 1 + assert len(second_chart.elements[0].points) == 5 diff --git a/packages/charts-python/tests/charts/test_unknown.py b/packages/charts-python/tests/charts/test_unknown.py new file mode 100644 index 0000000000..848afe983f --- /dev/null +++ b/packages/charts-python/tests/charts/test_unknown.py @@ -0,0 +1,37 @@ +import matplotlib.pyplot as plt + +from e2b_charts import chart_figure_to_chart +from e2b_charts.charts import Chart, ChartType + + +def _prep_chart_figure(): + # Create a figure and an axis + fig, ax = plt.subplots() + + # Create data for two concentric circles + circle1 = plt.Circle((0, 0), 1, color="blue", fill=False, linewidth=2) + circle2 = plt.Circle((0, 0), 2, color="red", fill=False, linewidth=2) + + # Add the circles to the axes + ax.add_artist(circle1) + ax.add_artist(circle2) + + # Set grid + ax.grid(True) + + # Set title + plt.title("Two Concentric Circles") + + return plt.gcf() + + +def test_unknown_charts(): + figure = _prep_chart_figure() + chart = chart_figure_to_chart(figure) + assert chart + + assert isinstance(chart, Chart) + assert chart.type == ChartType.UNKNOWN + assert chart.title == "Two Concentric Circles" + + assert len(chart.elements) == 0 diff --git a/packages/charts-python/tests/utils/test_detect_scale.py b/packages/charts-python/tests/utils/test_detect_scale.py new file mode 100644 index 0000000000..62eaaa64bb --- /dev/null +++ b/packages/charts-python/tests/utils/test_detect_scale.py @@ -0,0 +1,24 @@ +import datetime + +from matplotlib.dates import _SwitchableDateConverter + +from e2b_charts.charts.planar import PointChart + + +def test_detect_scale(): + datetime_converter = _SwitchableDateConverter() + scale = PointChart._detect_scale( + datetime_converter, "linear", 3 * [datetime.date.today()], ["1", "2", "3"] + ) + assert scale == "datetime" + + scale = PointChart._detect_scale(None, "linear", [1, 2, 3], ["1", "2", "3"]) + assert scale == "linear" + + scale = PointChart._detect_scale( + None, "linear", [0, 1, 2], ["First", "Second", "Third"] + ) + assert scale == "categorical" + + scale = PointChart._detect_scale(None, "log", [1, 10, 100], ["1", "10", "100"]) + assert scale == "log" diff --git a/packages/charts-python/tests/utils/test_is_grid_line.py b/packages/charts-python/tests/utils/test_is_grid_line.py new file mode 100644 index 0000000000..02dcd2fe06 --- /dev/null +++ b/packages/charts-python/tests/utils/test_is_grid_line.py @@ -0,0 +1,17 @@ +from matplotlib.lines import Line2D + +from e2b_charts.utils.filtering import is_grid_line + + +def test_is_grid_line(): + not_a_grid_line = Line2D([1, 2], [2, 3]) + assert not is_grid_line(not_a_grid_line) + + horizontal_grid_line = Line2D([1, 2], [2, 2]) + assert is_grid_line(horizontal_grid_line) + + vertical_grid_line = Line2D([1, 1], [2, 3]) + assert is_grid_line(vertical_grid_line) + + long_line = Line2D([1, 1, 1, 1], [2, 3, 4, 5]) + assert not is_grid_line(long_line) diff --git a/packages/charts-python/uv.lock b/packages/charts-python/uv.lock new file mode 100644 index 0000000000..89b7f4bd7f --- /dev/null +++ b/packages/charts-python/uv.lock @@ -0,0 +1,889 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "e2b-charts" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-dotenv" }, + { name = "python-dotenv" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "matplotlib", specifier = ">=3.10.3,<4" }, + { name = "numpy", specifier = ">=2.2.6,<3" }, + { name = "pydantic", specifier = ">=2.9.1,<3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.3,<10" }, + { name = "pytest-dotenv", specifier = ">=0.5.2,<0.6" }, + { name = "python-dotenv", specifier = ">=1.2.1,<2" }, + { name = "ruff", specifier = ">=0.11.12,<0.12" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fonttools" +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" }, + { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" }, + { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" }, + { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" }, + { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" }, + { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" }, + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/6f/340b04986e67aac6f66c5145ce68bf72c64bed30f92c8913499a6e6b8f99/matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", size = 8296625, upload-time = "2026-04-24T00:11:43.376Z" }, + { url = "https://files.pythonhosted.org/packages/bb/2f/127081eb83162053ebb9678ceac64220b93a663e0167432566e9c7c82aab/matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", size = 8188790, upload-time = "2026-04-24T00:11:46.556Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b7/d8bcec2626c35f96972bff656299fef4578113ea6193c8fdad324710410c/matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", size = 8769389, upload-time = "2026-04-24T00:11:48.959Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/b78e214a527ea732033b7f4d37f7afb504d74ba9d134bd47938230dfb8b1/matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", size = 9589657, upload-time = "2026-04-24T00:11:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/5f/15/5246f7b43beae19c74dfee651d58d6cc8112e06f77adb4e88cc04f2e3a23/matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", size = 9651983, upload-time = "2026-04-24T00:11:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/5acecfe672ba0fa1b8c0454f69ce155d1e6fc5852fa7206bf9afaf767121/matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", size = 8199701, upload-time = "2026-04-24T00:11:58.389Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2b/0e92ad0ac446633f928a1563db4aa8add407e1924faf0ded5b95b35afb27/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", size = 8293058, upload-time = "2026-04-24T00:13:56.339Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/74682fd369f5299ceda438fea2a0662e6383b85c9383fb9cdfcf04713e07/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", size = 8186627, upload-time = "2026-04-24T00:13:58.623Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e8/368aab88f3c4cd8992800f31abfe0670c3e47540ba20a97e9fdbcde594b3/matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", size = 8764117, upload-time = "2026-04-24T00:14:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, + { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-dotenv" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/b0/cafee9c627c1bae228eb07c9977f679b3a7cb111b488307ab9594ba9e4da/pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732", size = 3782, upload-time = "2020-06-16T12:38:03.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/da/9da67c67b3d0963160e3d2cbc7c38b6fae342670cc8e6d5936644b2cf944/pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f", size = 3993, upload-time = "2020-06-16T12:38:01.139Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] diff --git a/packages/code-interpreter-template/Makefile b/packages/code-interpreter-template/Makefile new file mode 100644 index 0000000000..d82f972260 --- /dev/null +++ b/packages/code-interpreter-template/Makefile @@ -0,0 +1,8 @@ +start-template-server: + docker run --rm -e E2B_LOCAL=true -p 49999:49999 -it $$(python build_docker.py | docker build -q . -f -) + +kill-template-server: + docker kill $(shell docker ps --filter expose=49999 --format {{.ID}}) + +debug-template: + python build_debug.py && python debug_logs.py diff --git a/packages/code-interpreter-template/README.md b/packages/code-interpreter-template/README.md new file mode 100644 index 0000000000..bf1e850550 --- /dev/null +++ b/packages/code-interpreter-template/README.md @@ -0,0 +1,126 @@ +# Code Interpreter + +Run the commands below from `packages/code-interpreter-template`. + +## Building the production template + +To build the official `code-interpreter-v1` template from this package, use +`build_prod.py`. This is the script CI and releases run. + +1. Install the build dependencies: + +``` +pip install -r requirements-dev.txt +``` + +2. Provide your credentials in `.env`: + +``` +E2B_API_KEY=e2b_*** +``` + +3. Build the template: + +``` +python build_prod.py +``` + +Set `SKIP_CACHE=true` to force a clean rebuild that ignores the layer cache: + +``` +SKIP_CACHE=true python build_prod.py +``` + +If you want to customize the Code Interpreter sandbox (e.g.: add a preinstalled package) you can do that by creating a [custom sandbox template](https://e2b.dev/docs/template/quickstart). + +## Creating a custom template + +1. Install E2B SDK + +``` +pip install e2b dotenv +``` + +2. Create a custom sandbox template: + +**template.py** + +```python +from e2b import Template + +template = Template().from_template("code-interpreter-v1") +``` + +3. Create a build script: + +**build.py** + +```python +from dotenv import load_dotenv +from .template import template +from e2b import Template, default_build_logger + +load_dotenv() + +Template.build( + template, + alias="code-interpreter-custom", + cpu_count=2, + memory_mb=2048, + on_build_logs=default_build_logger(), +) +``` + +4. Set your environment variables in a `.env` file (loaded by `load_dotenv()`): + +``` +E2B_API_KEY=e2b_*** +``` + +5. Build the template: + +``` +python build.py +``` + +6. Use the custom template: + +```python +from e2b import Sandbox + +sbx = Sandbox.create(template="code-interpreter-custom") +execution = sbx.run_code("print('Hello, World!')") +print(execution.logs.stdout) +``` + +## Debugging a server that won't start + +The template runs Jupyter and the code-interpreter server as **systemd** +services (`systemd/jupyter.service`, `systemd/code-interpreter.service`). This is +the path CI and production use — note it is *different* from `make +start-template-server`, which runs the Docker `start-up.sh` path. The two can +diverge, so a server that boots fine under Docker may still fail under systemd. + +When a build fails its readiness check (`Waiting for template to be ready ... +timed out`), the real cause is in the service journals. To see them: + +``` +make debug-template +``` + +This builds a debug template (gated on a fixed timeout instead of `/health`, so +it finalizes even while the server is crash-looping), spawns a sandbox, and +prints `systemctl status` + the full `journalctl` for both services. It needs +`.env` with your `E2B_API_KEY` and the deps from `requirements-dev.txt`. + +The debug build also applies a systemd drop-in that routes Jupyter's stdout to +the journal (`make_template(debug=True)`). Production builds keep +`StandardOutput=null`, so Jupyter's request/error logs are only captured in the +debug template. + +Inside a running sandbox you can also inspect things directly: + +``` +journalctl -u jupyter -u code-interpreter +systemctl status code-interpreter +``` diff --git a/packages/code-interpreter-template/build_ci.py b/packages/code-interpreter-template/build_ci.py new file mode 100644 index 0000000000..13c0c31149 --- /dev/null +++ b/packages/code-interpreter-template/build_ci.py @@ -0,0 +1,19 @@ +import os +from e2b import Template, default_build_logger +from template import make_template + +build_info = Template.build( + make_template(), + alias=os.environ["E2B_TESTS_TEMPLATE"], + cpu_count=2, + memory_mb=2048, + on_build_logs=default_build_logger(), +) + +template_id = build_info.template_id +print(f"Built template ID: {template_id}") + +github_output = os.getenv("GITHUB_OUTPUT") +if github_output: + with open(github_output, "a", encoding="utf-8") as fh: + fh.write(f"template_id={template_id}\n") diff --git a/packages/code-interpreter-template/build_debug.py b/packages/code-interpreter-template/build_debug.py new file mode 100644 index 0000000000..f4dc63b43f --- /dev/null +++ b/packages/code-interpreter-template/build_debug.py @@ -0,0 +1,23 @@ +import os + +from dotenv import load_dotenv +from e2b import Template, default_build_logger, wait_for_timeout +from template import make_template + +load_dotenv() + +alias = os.getenv("E2B_DEBUG_TEMPLATE", "code-interpreter-debug") + +Template.build( + make_template( + kernels=["python", "javascript"], + ready=wait_for_timeout(60_000), + debug=True, + ), + alias=alias, + cpu_count=2, + memory_mb=2048, + on_build_logs=default_build_logger(min_level="debug"), +) + +print(f"Built debug template: {alias}") diff --git a/packages/code-interpreter-template/build_docker.py b/packages/code-interpreter-template/build_docker.py new file mode 100644 index 0000000000..7d1438a851 --- /dev/null +++ b/packages/code-interpreter-template/build_docker.py @@ -0,0 +1,5 @@ +from template import make_template +from e2b import Template + +tmp = make_template(kernels=["python", "javascript"], is_docker=True) +print(Template.to_dockerfile(tmp)) diff --git a/packages/code-interpreter-template/build_prod.py b/packages/code-interpreter-template/build_prod.py new file mode 100644 index 0000000000..1615ebff61 --- /dev/null +++ b/packages/code-interpreter-template/build_prod.py @@ -0,0 +1,18 @@ +import os + +from dotenv import load_dotenv +from e2b import Template, default_build_logger +from template import make_template + +load_dotenv() + +skip_cache = os.getenv("SKIP_CACHE", "false").lower() == "true" + +Template.build( + make_template(), + alias="code-interpreter-v1", + cpu_count=2, + memory_mb=2048, + skip_cache=skip_cache, + on_build_logs=default_build_logger(), +) diff --git a/packages/code-interpreter-template/build_test.py b/packages/code-interpreter-template/build_test.py new file mode 100644 index 0000000000..a11128ca39 --- /dev/null +++ b/packages/code-interpreter-template/build_test.py @@ -0,0 +1,13 @@ +from dotenv import load_dotenv +from e2b import Template, default_build_logger +from template import make_template + +load_dotenv() + +Template.build( + make_template(kernels=["python", "javascript"]), + alias="code-interpreter-dev", + cpu_count=2, + memory_mb=2048, + on_build_logs=default_build_logger(min_level="debug"), +) diff --git a/packages/code-interpreter-template/debug_logs.py b/packages/code-interpreter-template/debug_logs.py new file mode 100644 index 0000000000..2cd74aff65 --- /dev/null +++ b/packages/code-interpreter-template/debug_logs.py @@ -0,0 +1,35 @@ +import os + +from dotenv import load_dotenv +from e2b import Sandbox + +load_dotenv() + +alias = os.getenv("E2B_DEBUG_TEMPLATE", "code-interpreter-debug") + +sbx = Sandbox.create(template=alias, timeout=600) +print(f"sandbox: {sbx.sandbox_id}") + +CMDS = [ + "systemctl --no-pager status jupyter || true", + "systemctl --no-pager status code-interpreter || true", + "journalctl --no-pager -u jupyter || true", + "journalctl --no-pager -u code-interpreter || true", + "curl --unix-socket /run/e2b-jupyter/server.sock -s --max-time 3 -o /dev/null -w 'jupyter socket -> %{http_code}\\n' http://localhost/api/status || true", + "curl -s --max-time 3 -o /dev/null -w 'server :49999 -> %{http_code}\\n' http://localhost:49999/health || true", +] + +try: + for cmd in CMDS: + print(f"\n===== $ {cmd} =====") + try: + result = sbx.commands.run(f"sudo bash -lc {cmd!r}", timeout=60) + if result.stdout: + print(result.stdout) + if result.stderr: + print("[stderr]", result.stderr) + except Exception as e: + # Keep going so one slow/failed command doesn't skip the rest. + print(f"[command failed] {e}") +finally: + sbx.kill() diff --git a/packages/code-interpreter-template/ipython_kernel_config.py b/packages/code-interpreter-template/ipython_kernel_config.py new file mode 100644 index 0000000000..26900faa92 --- /dev/null +++ b/packages/code-interpreter-template/ipython_kernel_config.py @@ -0,0 +1,969 @@ +# Configuration file for ipython-kernel. + +c = get_config() # noqa + +## Set the color scheme (NoColor, Neutral, Linux, or LightBG). +# Choices: any of ['Neutral', 'NoColor', 'LightBG', 'Linux'] (case-insensitive) +# Default: 'Neutral' +c.InteractiveShell.colors = "NoColor" + + +# ------------------------------------------------------------------------------ +# ConnectionFileMixin(LoggingConfigurable) configuration +# ------------------------------------------------------------------------------ +## Mixin for configurable classes that work with connection files + +## JSON file in which to store connection info [default: kernel-.json] +# +# This file will contain the IP, ports, and authentication key needed to connect +# clients to this kernel. By default, this file will be created in the security dir +# of the current profile, but can be specified by absolute path. +# Default: '' +# c.ConnectionFileMixin.connection_file = '' + +## set the control (ROUTER) port [default: random] +# Default: 0 +# c.ConnectionFileMixin.control_port = 0 + +## set the heartbeat port [default: random] +# Default: 0 +# c.ConnectionFileMixin.hb_port = 0 + +## set the iopub (PUB) port [default: random] +# Default: 0 +# c.ConnectionFileMixin.iopub_port = 0 + +## Set the kernel's IP address [default localhost]. +# If the IP address is something other than localhost, then +# Consoles on other machines will be able to connect +# to the Kernel, so be careful! +# Default: '' +# c.ConnectionFileMixin.ip = '' + +## set the shell (ROUTER) port [default: random] +# Default: 0 +# c.ConnectionFileMixin.shell_port = 0 + +## set the stdin (ROUTER) port [default: random] +# Default: 0 +# c.ConnectionFileMixin.stdin_port = 0 + +# Choices: any of ['tcp', 'ipc'] (case-insensitive) +# Default: 'tcp' +# c.ConnectionFileMixin.transport = 'tcp' + +# ------------------------------------------------------------------------------ +# InteractiveShellApp(Configurable) configuration +# ------------------------------------------------------------------------------ +## A Mixin for applications that start InteractiveShell instances. +# +# Provides configurables for loading extensions and executing files +# as part of configuring a Shell environment. +# +# The following methods should be called by the :meth:`initialize` method +# of the subclass: +# +# - :meth:`init_path` +# - :meth:`init_shell` (to be implemented by the subclass) +# - :meth:`init_gui_pylab` +# - :meth:`init_extensions` +# - :meth:`init_code` + +## Execute the given command string. +# Default: '' +# c.InteractiveShellApp.code_to_run = '' + +## Run the file referenced by the PYTHONSTARTUP environment +# variable at IPython startup. +# Default: True +# c.InteractiveShellApp.exec_PYTHONSTARTUP = True + +## List of files to run at IPython startup. +# Default: [] +# c.InteractiveShellApp.exec_files = [] + +## lines of code to run at IPython startup. +# Default: [] +# c.InteractiveShellApp.exec_lines = [] + +## A list of dotted module names of IPython extensions to load. +# Default: [] +# c.InteractiveShellApp.extensions = [] + +## Dotted module name(s) of one or more IPython extensions to load. +# +# For specifying extra extensions to load on the command-line. +# +# .. versionadded:: 7.10 +# Default: [] +# c.InteractiveShellApp.extra_extensions = [] + +## A file to be run +# Default: '' +# c.InteractiveShellApp.file_to_run = '' + +## Enable GUI event loop integration with any of ('asyncio', 'glut', 'gtk', +# 'gtk2', 'gtk3', 'gtk4', 'osx', 'pyglet', 'qt', 'qt5', 'qt6', 'tk', 'wx', +# 'gtk2', 'qt4'). +# Choices: any of ['asyncio', 'glut', 'gtk', 'gtk2', 'gtk3', 'gtk4', 'osx', 'pyglet', 'qt', 'qt5', 'qt6', 'tk', 'wx', 'gtk2', 'qt4'] (case-insensitive) or None +# Default: None +# c.InteractiveShellApp.gui = None + +## Should variables loaded at startup (by startup files, exec_lines, etc.) +# be hidden from tools like %who? +# Default: True +# c.InteractiveShellApp.hide_initial_ns = True + +## If True, IPython will not add the current working directory to sys.path. +# When False, the current working directory is added to sys.path, allowing imports +# of modules defined in the current directory. +# Default: False +# c.InteractiveShellApp.ignore_cwd = False + +## Configure matplotlib for interactive use with +# the default matplotlib backend. +# Choices: any of ['auto', 'agg', 'gtk', 'gtk3', 'gtk4', 'inline', 'ipympl', 'nbagg', 'notebook', 'osx', 'pdf', 'ps', 'qt', 'qt4', 'qt5', 'qt6', 'svg', 'tk', 'webagg', 'widget', 'wx'] (case-insensitive) or None +# Default: None +# c.InteractiveShellApp.matplotlib = None + +## Run the module as a script. +# Default: '' +# c.InteractiveShellApp.module_to_run = '' + +## Pre-load matplotlib and numpy for interactive use, +# selecting a particular matplotlib backend and loop integration. +# Choices: any of ['auto', 'agg', 'gtk', 'gtk3', 'gtk4', 'inline', 'ipympl', 'nbagg', 'notebook', 'osx', 'pdf', 'ps', 'qt', 'qt4', 'qt5', 'qt6', 'svg', 'tk', 'webagg', 'widget', 'wx'] (case-insensitive) or None +# Default: None +# c.InteractiveShellApp.pylab = None + +## If true, IPython will populate the user namespace with numpy, pylab, etc. +# and an ``import *`` is done from numpy and pylab, when using pylab mode. +# +# When False, pylab mode should not import any names into the user +# namespace. +# Default: True +# c.InteractiveShellApp.pylab_import_all = True + +## Reraise exceptions encountered loading IPython extensions? +# Default: False +# c.InteractiveShellApp.reraise_ipython_extension_failures = False + +# ------------------------------------------------------------------------------ +# Application(SingletonConfigurable) configuration +# ------------------------------------------------------------------------------ +## This is an application. + +## The date format used by logging formatters for %(asctime)s +# Default: '%Y-%m-%d %H:%M:%S' +# c.Application.log_datefmt = '%Y-%m-%d %H:%M:%S' + +## The Logging format template +# Default: '[%(name)s]%(highlevel)s %(message)s' +# c.Application.log_format = '[%(name)s]%(highlevel)s %(message)s' + +## Set the log level by value or name. +# Choices: any of [0, 10, 20, 30, 40, 50, 'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'] +# Default: 30 +# c.Application.log_level = 30 + +## Configure additional log handlers. +# +# The default stderr logs handler is configured by the log_level, log_datefmt +# and log_format settings. +# +# This configuration can be used to configure additional handlers (e.g. to +# output the log to a file) or for finer control over the default handlers. +# +# If provided this should be a logging configuration dictionary, for more +# information see: +# https://docs.python.org/3/library/logging.config.html#logging-config- +# dictschema +# +# This dictionary is merged with the base logging configuration which defines +# the following: +# +# * A logging formatter intended for interactive use called +# ``console``. +# * A logging handler that writes to stderr called +# ``console`` which uses the formatter ``console``. +# * A logger with the name of this application set to ``DEBUG`` +# level. +# +# This example adds a new handler that writes to a file: +# +# .. code-block:: python +# +# c.Application.logging_config = { +# "handlers": { +# "file": { +# "class": "logging.FileHandler", +# "level": "DEBUG", +# "filename": "", +# } +# }, +# "loggers": { +# "": { +# "level": "DEBUG", +# # NOTE: if you don't list the default "console" +# # handler here then it will be disabled +# "handlers": ["console", "file"], +# }, +# }, +# } +# Default: {} +# c.Application.logging_config = {} + +## Instead of starting the Application, dump configuration to stdout +# Default: False +# c.Application.show_config = False + +## Instead of starting the Application, dump configuration to stdout (as JSON) +# Default: False +# c.Application.show_config_json = False + +# ------------------------------------------------------------------------------ +# BaseIPythonApplication(Application) configuration +# ------------------------------------------------------------------------------ +# Default: False +# c.BaseIPythonApplication.add_ipython_dir_to_sys_path = False + +## Whether to create profile dir if it doesn't exist +# Default: False +# c.BaseIPythonApplication.auto_create = False + +## Whether to install the default config files into the profile dir. +# If a new profile is being created, and IPython contains config files for that +# profile, then they will be staged into the new directory. Otherwise, +# default config files will be automatically generated. +# Default: False +# c.BaseIPythonApplication.copy_config_files = False + +## Path to an extra config file to load. +# +# If specified, load this config file in addition to any other IPython +# config. +# Default: '' +# c.BaseIPythonApplication.extra_config_file = '' + +## The name of the IPython directory. This directory is used for logging +# configuration (through profiles), history storage, etc. The default is usually +# $HOME/.ipython. This option can also be specified through the environment +# variable IPYTHONDIR. +# Default: '' +# c.BaseIPythonApplication.ipython_dir = '' + +## The date format used by logging formatters for %(asctime)s +# See also: Application.log_datefmt +# c.BaseIPythonApplication.log_datefmt = '%Y-%m-%d %H:%M:%S' + +## The Logging format template +# See also: Application.log_format +# c.BaseIPythonApplication.log_format = '[%(name)s]%(highlevel)s %(message)s' + +## Set the log level by value or name. +# See also: Application.log_level +# c.BaseIPythonApplication.log_level = 30 + +## +# See also: Application.logging_config +# c.BaseIPythonApplication.logging_config = {} + +## Whether to overwrite existing config files when copying +# Default: False +# c.BaseIPythonApplication.overwrite = False + +## The IPython profile to use. +# Default: 'default' +# c.BaseIPythonApplication.profile = 'default' + +## Instead of starting the Application, dump configuration to stdout +# See also: Application.show_config +# c.BaseIPythonApplication.show_config = False + +## Instead of starting the Application, dump configuration to stdout (as JSON) +# See also: Application.show_config_json +# c.BaseIPythonApplication.show_config_json = False + +## Create a massive crash report when IPython encounters what may be an +# internal error. The default is to append a short message to the +# usual traceback +# Default: False +# c.BaseIPythonApplication.verbose_crash = False + +# ------------------------------------------------------------------------------ +# IPKernelApp(BaseIPythonApplication, InteractiveShellApp, ConnectionFileMixin) configuration +# ------------------------------------------------------------------------------ +## The IPYKernel application class. + +# See also: BaseIPythonApplication.add_ipython_dir_to_sys_path +# c.IPKernelApp.add_ipython_dir_to_sys_path = False + +## Whether to create profile dir if it doesn't exist +# See also: BaseIPythonApplication.auto_create +# c.IPKernelApp.auto_create = False + +## Attempt to capture and forward low-level output, e.g. produced by Extension +# libraries. +# Default: True +# c.IPKernelApp.capture_fd_output = True + +## Execute the given command string. +# See also: InteractiveShellApp.code_to_run +# c.IPKernelApp.code_to_run = '' + +## JSON file in which to store connection info [default: kernel-.json] +# See also: ConnectionFileMixin.connection_file +# c.IPKernelApp.connection_file = '' + +## set the control (ROUTER) port [default: random] +# See also: ConnectionFileMixin.control_port +# c.IPKernelApp.control_port = 0 + +## Whether to install the default config files into the profile dir. +# See also: BaseIPythonApplication.copy_config_files +# c.IPKernelApp.copy_config_files = False + +## The importstring for the DisplayHook factory +# Default: 'ipykernel.displayhook.ZMQDisplayHook' +# c.IPKernelApp.displayhook_class = 'ipykernel.displayhook.ZMQDisplayHook' + +## Run the file referenced by the PYTHONSTARTUP environment +# See also: InteractiveShellApp.exec_PYTHONSTARTUP +# c.IPKernelApp.exec_PYTHONSTARTUP = True + +## List of files to run at IPython startup. +# See also: InteractiveShellApp.exec_files +# c.IPKernelApp.exec_files = [] + +## lines of code to run at IPython startup. +# See also: InteractiveShellApp.exec_lines +# c.IPKernelApp.exec_lines = [] + +## A list of dotted module names of IPython extensions to load. +# See also: InteractiveShellApp.extensions +# c.IPKernelApp.extensions = [] + +## Path to an extra config file to load. +# See also: BaseIPythonApplication.extra_config_file +# c.IPKernelApp.extra_config_file = '' + +## +# See also: InteractiveShellApp.extra_extensions +# c.IPKernelApp.extra_extensions = [] + +## A file to be run +# See also: InteractiveShellApp.file_to_run +# c.IPKernelApp.file_to_run = '' + +## Enable GUI event loop integration with any of ('asyncio', 'glut', 'gtk', +# 'gtk2', 'gtk3', 'gtk4', 'osx', 'pyglet', 'qt', 'qt5', 'qt6', 'tk', 'wx', +# 'gtk2', 'qt4'). +# See also: InteractiveShellApp.gui +# c.IPKernelApp.gui = None + +## set the heartbeat port [default: random] +# See also: ConnectionFileMixin.hb_port +# c.IPKernelApp.hb_port = 0 + +## Should variables loaded at startup (by startup files, exec_lines, etc.) +# See also: InteractiveShellApp.hide_initial_ns +# c.IPKernelApp.hide_initial_ns = True + +## If True, IPython will not add the current working directory to sys.path. +# See also: InteractiveShellApp.ignore_cwd +# c.IPKernelApp.ignore_cwd = False + +## ONLY USED ON WINDOWS +# Interrupt this process when the parent is signaled. +# Default: 0 +# c.IPKernelApp.interrupt = 0 + +## set the iopub (PUB) port [default: random] +# See also: ConnectionFileMixin.iopub_port +# c.IPKernelApp.iopub_port = 0 + +## Set the kernel's IP address [default localhost]. +# See also: ConnectionFileMixin.ip +# c.IPKernelApp.ip = '' + +## +# See also: BaseIPythonApplication.ipython_dir +# c.IPKernelApp.ipython_dir = '' + +## The Kernel subclass to be used. +# +# This should allow easy reuse of the IPKernelApp entry point +# to configure and launch kernels other than IPython's own. +# Default: 'ipykernel.ipkernel.IPythonKernel' +# c.IPKernelApp.kernel_class = 'ipykernel.ipkernel.IPythonKernel' + +## The date format used by logging formatters for %(asctime)s +# See also: Application.log_datefmt +# c.IPKernelApp.log_datefmt = '%Y-%m-%d %H:%M:%S' + +## The Logging format template +# See also: Application.log_format +# c.IPKernelApp.log_format = '[%(name)s]%(highlevel)s %(message)s' + +## Set the log level by value or name. +# See also: Application.log_level +# c.IPKernelApp.log_level = 30 + +## +# See also: Application.logging_config +# c.IPKernelApp.logging_config = {} + +## Configure matplotlib for interactive use with +# See also: InteractiveShellApp.matplotlib +# c.IPKernelApp.matplotlib = None + +## Run the module as a script. +# See also: InteractiveShellApp.module_to_run +# c.IPKernelApp.module_to_run = '' + +## redirect stderr to the null device +# Default: False +# c.IPKernelApp.no_stderr = False + +## redirect stdout to the null device +# Default: False +# c.IPKernelApp.no_stdout = False + +## The importstring for the OutStream factory +# Default: 'ipykernel.iostream.OutStream' +# c.IPKernelApp.outstream_class = 'ipykernel.iostream.OutStream' + +## Whether to overwrite existing config files when copying +# See also: BaseIPythonApplication.overwrite +# c.IPKernelApp.overwrite = False + +## kill this process if its parent dies. On Windows, the argument +# specifies the HANDLE of the parent process, otherwise it is simply boolean. +# Default: 0 +# c.IPKernelApp.parent_handle = 0 + +## The IPython profile to use. +# See also: BaseIPythonApplication.profile +# c.IPKernelApp.profile = 'default' + +## Pre-load matplotlib and numpy for interactive use, +# See also: InteractiveShellApp.pylab +# c.IPKernelApp.pylab = None + +## If true, IPython will populate the user namespace with numpy, pylab, etc. +# See also: InteractiveShellApp.pylab_import_all +# c.IPKernelApp.pylab_import_all = True + +## Only send stdout/stderr to output stream +# Default: True +# c.IPKernelApp.quiet = True + +## Reraise exceptions encountered loading IPython extensions? +# See also: InteractiveShellApp.reraise_ipython_extension_failures +# c.IPKernelApp.reraise_ipython_extension_failures = False + +## set the shell (ROUTER) port [default: random] +# See also: ConnectionFileMixin.shell_port +# c.IPKernelApp.shell_port = 0 + +## Instead of starting the Application, dump configuration to stdout +# See also: Application.show_config +# c.IPKernelApp.show_config = False + +## Instead of starting the Application, dump configuration to stdout (as JSON) +# See also: Application.show_config_json +# c.IPKernelApp.show_config_json = False + +## set the stdin (ROUTER) port [default: random] +# See also: ConnectionFileMixin.stdin_port +# c.IPKernelApp.stdin_port = 0 + +# See also: ConnectionFileMixin.transport +# c.IPKernelApp.transport = 'tcp' + +## Set main event loop. +# Default: False +# c.IPKernelApp.trio_loop = False + +## Create a massive crash report when IPython encounters what may be an +# See also: BaseIPythonApplication.verbose_crash +# c.IPKernelApp.verbose_crash = False + +# ------------------------------------------------------------------------------ +# Kernel(SingletonConfigurable) configuration +# ------------------------------------------------------------------------------ +## The base kernel class. + +## Whether to use appnope for compatibility with OS X App Nap. +# +# Only affects OS X >= 10.9. +# Default: True +# c.Kernel._darwin_app_nap = True + +# Default: 0.0005 +# c.Kernel._execute_sleep = 0.0005 + +# Default: 0.01 +# c.Kernel._poll_interval = 0.01 + +## Set to False if you want to debug python standard and dependent libraries. +# Default: True +# c.Kernel.debug_just_my_code = True + +## time (in seconds) to wait for messages to arrive +# when aborting queued requests after an error. +# +# Requests that arrive within this window after an error +# will be cancelled. +# +# Increase in the event of unusually slow network +# causing significant delays, +# which can manifest as e.g. "Run all" in a notebook +# aborting some, but not all, messages after an error. +# Default: 0.0 +# c.Kernel.stop_on_error_timeout = 0.0 + +# ------------------------------------------------------------------------------ +# IPythonKernel(Kernel) configuration +# ------------------------------------------------------------------------------ +## The IPython Kernel class. + +## Whether to use appnope for compatibility with OS X App Nap. +# See also: Kernel._darwin_app_nap +# c.IPythonKernel._darwin_app_nap = True + +# See also: Kernel._execute_sleep +# c.IPythonKernel._execute_sleep = 0.0005 + +# See also: Kernel._poll_interval +# c.IPythonKernel._poll_interval = 0.01 + +## Set to False if you want to debug python standard and dependent libraries. +# See also: Kernel.debug_just_my_code +# c.IPythonKernel.debug_just_my_code = True + +# Default: [{'text': 'Python Reference', 'url': 'https://docs.python.org/3.10'}, {'text': 'IPython Reference', 'url': 'https://ipython.org/documentation.html'}, {'text': 'NumPy Reference', 'url': 'https://docs.scipy.org/doc/numpy/reference/'}, {'text': 'SciPy Reference', 'url': 'https://docs.scipy.org/doc/scipy/reference/'}, {'text': 'Matplotlib Reference', 'url': 'https://matplotlib.org/contents.html'}, {'text': 'SymPy Reference', 'url': 'http://docs.sympy.org/latest/index.html'}, {'text': 'pandas Reference', 'url': 'https://pandas.pydata.org/pandas-docs/stable/'}] +# c.IPythonKernel.help_links = [{'text': 'Python Reference', 'url': 'https://docs.python.org/3.10'}, {'text': 'IPython Reference', 'url': 'https://ipython.org/documentation.html'}, {'text': 'NumPy Reference', 'url': 'https://docs.scipy.org/doc/numpy/reference/'}, {'text': 'SciPy Reference', 'url': 'https://docs.scipy.org/doc/scipy/reference/'}, {'text': 'Matplotlib Reference', 'url': 'https://matplotlib.org/contents.html'}, {'text': 'SymPy Reference', 'url': 'http://docs.sympy.org/latest/index.html'}, {'text': 'pandas Reference', 'url': 'https://pandas.pydata.org/pandas-docs/stable/'}] + +## time (in seconds) to wait for messages to arrive +# See also: Kernel.stop_on_error_timeout +# c.IPythonKernel.stop_on_error_timeout = 0.0 + +## Set this flag to False to deactivate the use of experimental IPython +# completion APIs. +# Default: True +# c.IPythonKernel.use_experimental_completions = True + +# ------------------------------------------------------------------------------ +# InteractiveShell(SingletonConfigurable) configuration +# ------------------------------------------------------------------------------ +## An enhanced, interactive shell for Python. + +## 'all', 'last', 'last_expr' or 'none', 'last_expr_or_assign' specifying which +# nodes should be run interactively (displaying output from expressions). +# Choices: any of ['all', 'last', 'last_expr', 'none', 'last_expr_or_assign'] +# Default: 'last_expr' +# c.InteractiveShell.ast_node_interactivity = 'last_expr' + +## A list of ast.NodeTransformer subclass instances, which will be applied to +# user input before code is run. +# Default: [] +# c.InteractiveShell.ast_transformers = [] + +## Automatically run await statement in the top level repl. +# Default: True +# c.InteractiveShell.autoawait = True + +## Make IPython automatically call any callable object even if you didn't type +# explicit parentheses. For example, 'str 43' becomes 'str(43)' automatically. +# The value can be '0' to disable the feature, '1' for 'smart' autocall, where +# it is not applied if there are no more arguments on the line, and '2' for +# 'full' autocall, where all callable objects are automatically called (even if +# no arguments are present). +# Choices: any of [0, 1, 2] +# Default: 0 +# c.InteractiveShell.autocall = 0 + +## Autoindent IPython code entered interactively. +# Default: True +# c.InteractiveShell.autoindent = True + +## Enable magic commands to be called without the leading %. +# Default: True +# c.InteractiveShell.automagic = True + +## The part of the banner to be printed before the profile +# Default: "Python 3.10.14 (main, Mar 25 2024, 21:45:25) [GCC 12.2.0]\nType 'copyright', 'credits' or 'license' for more information\nIPython 8.22.2 -- An enhanced Interactive Python. Type '?' for help.\n" +# c.InteractiveShell.banner1 = "Python 3.10.14 (main, Mar 25 2024, 21:45:25) [GCC 12.2.0]\nType 'copyright', 'credits' or 'license' for more information\nIPython 8.22.2 -- An enhanced Interactive Python. Type '?' for help.\n" + +## The part of the banner to be printed after the profile +# Default: '' +# c.InteractiveShell.banner2 = '' + +## Set the size of the output cache. The default is 1000, you can change it +# permanently in your config file. Setting it to 0 completely disables the +# caching system, and the minimum value accepted is 3 (if you provide a value +# less than 3, it is reset to 0 and a warning is issued). This limit is defined +# because otherwise you'll spend more time re-flushing a too small cache than +# working +# Default: 1000 +# c.InteractiveShell.cache_size = 1000 + +## Use colors for displaying information about objects. Because this information +# is passed through a pager (like 'less'), and some pagers get confused with +# color codes, this capability can be turned off. +# Default: True +# c.InteractiveShell.color_info = True +# Default: False +# c.InteractiveShell.debug = False + +## Don't call post-execute functions that have failed in the past. +# Default: False +# c.InteractiveShell.disable_failing_post_execute = False + +## If True, anything that would be passed to the pager +# will be displayed as regular output instead. +# Default: False +# c.InteractiveShell.display_page = False + +## (Provisional API) enables html representation in mime bundles sent to pagers. +# Default: False +# c.InteractiveShell.enable_html_pager = False + +## Total length of command history +# Default: 10000 +# c.InteractiveShell.history_length = 10000 + +## The number of saved history entries to be loaded into the history buffer at +# startup. +# Default: 1000 +# c.InteractiveShell.history_load_length = 1000 + +## Class to use to instantiate the shell inspector +# Default: 'IPython.core.oinspect.Inspector' +# c.InteractiveShell.inspector_class = 'IPython.core.oinspect.Inspector' + +# Default: '' +# c.InteractiveShell.ipython_dir = '' + +## Start logging to the given file in append mode. Use `logfile` to specify a log +# file to **overwrite** logs to. +# Default: '' +# c.InteractiveShell.logappend = '' + +## The name of the logfile to use. +# Default: '' +# c.InteractiveShell.logfile = '' + +## Start logging to the default log file in overwrite mode. Use `logappend` to +# specify a log file to **append** logs to. +# Default: False +# c.InteractiveShell.logstart = False + +## Select the loop runner that will be used to execute top-level asynchronous +# code +# Default: 'IPython.core.interactiveshell._asyncio_runner' +# c.InteractiveShell.loop_runner = 'IPython.core.interactiveshell._asyncio_runner' + +# Choices: any of [0, 1, 2] +# Default: 0 +# c.InteractiveShell.object_info_string_level = 0 + +## Automatically call the pdb debugger after every exception. +# Default: False +# c.InteractiveShell.pdb = False + +# Default: False +# c.InteractiveShell.quiet = False + +# Default: '\n' +# c.InteractiveShell.separate_in = '\n' + +# Default: '' +# c.InteractiveShell.separate_out = '' + +# Default: '' +# c.InteractiveShell.separate_out2 = '' + +## Show rewritten input, e.g. for autocall. +# Default: True +# c.InteractiveShell.show_rewritten_input = True + +## Enables rich html representation of docstrings. (This requires the docrepr +# module). +# Default: False +# c.InteractiveShell.sphinxify_docstring = False + +## Warn if running in a virtual environment with no IPython installed (so IPython +# from the global environment is used). +# Default: True +# c.InteractiveShell.warn_venv = True + +# Default: True +# c.InteractiveShell.wildcards_case_sensitive = True + +## Switch modes for the IPython exception handlers. +# Choices: any of ['Context', 'Plain', 'Verbose', 'Minimal'] (case-insensitive) +# Default: 'Context' +# c.InteractiveShell.xmode = 'Context' + +# ------------------------------------------------------------------------------ +# ZMQInteractiveShell(InteractiveShell) configuration +# ------------------------------------------------------------------------------ +## A subclass of InteractiveShell for ZMQ. + +## +# See also: InteractiveShell.ast_node_interactivity +# c.ZMQInteractiveShell.ast_node_interactivity = 'last_expr' + +## +# See also: InteractiveShell.ast_transformers +# c.ZMQInteractiveShell.ast_transformers = [] + +## +# See also: InteractiveShell.autoawait +# c.ZMQInteractiveShell.autoawait = True + +## +# See also: InteractiveShell.autocall +# c.ZMQInteractiveShell.autocall = 0 + +## +# See also: InteractiveShell.automagic +# c.ZMQInteractiveShell.automagic = True + +## The part of the banner to be printed before the profile +# See also: InteractiveShell.banner1 +# c.ZMQInteractiveShell.banner1 = "Python 3.10.14 (main, Mar 25 2024, 21:45:25) [GCC 12.2.0]\nType 'copyright', 'credits' or 'license' for more information\nIPython 8.22.2 -- An enhanced Interactive Python. Type '?' for help.\n" + +## The part of the banner to be printed after the profile +# See also: InteractiveShell.banner2 +# c.ZMQInteractiveShell.banner2 = '' + +## +# See also: InteractiveShell.cache_size +# c.ZMQInteractiveShell.cache_size = 1000 + +## +# See also: InteractiveShell.color_info +# c.ZMQInteractiveShell.color_info = True + +## Set the color scheme (NoColor, Neutral, Linux, or LightBG). +# See also: InteractiveShell.colors +# c.ZMQInteractiveShell.colors = 'Neutral' + +# See also: InteractiveShell.debug +# c.ZMQInteractiveShell.debug = False + +## Don't call post-execute functions that have failed in the past. +# See also: InteractiveShell.disable_failing_post_execute +# c.ZMQInteractiveShell.disable_failing_post_execute = False + +## If True, anything that would be passed to the pager +# See also: InteractiveShell.display_page +# c.ZMQInteractiveShell.display_page = False + +## +# See also: InteractiveShell.enable_html_pager +# c.ZMQInteractiveShell.enable_html_pager = False + +## Total length of command history +# See also: InteractiveShell.history_length +# c.ZMQInteractiveShell.history_length = 10000 + +## +# See also: InteractiveShell.history_load_length +# c.ZMQInteractiveShell.history_load_length = 1000 + +## Class to use to instantiate the shell inspector +# See also: InteractiveShell.inspector_class +# c.ZMQInteractiveShell.inspector_class = 'IPython.core.oinspect.Inspector' + +# See also: InteractiveShell.ipython_dir +# c.ZMQInteractiveShell.ipython_dir = '' + +## +# See also: InteractiveShell.logappend +# c.ZMQInteractiveShell.logappend = '' + +## +# See also: InteractiveShell.logfile +# c.ZMQInteractiveShell.logfile = '' + +## +# See also: InteractiveShell.logstart +# c.ZMQInteractiveShell.logstart = False + +## Select the loop runner that will be used to execute top-level asynchronous +# code +# See also: InteractiveShell.loop_runner +# c.ZMQInteractiveShell.loop_runner = 'IPython.core.interactiveshell._asyncio_runner' + +# See also: InteractiveShell.object_info_string_level +# c.ZMQInteractiveShell.object_info_string_level = 0 + +## +# See also: InteractiveShell.pdb +# c.ZMQInteractiveShell.pdb = False + +# See also: InteractiveShell.quiet +# c.ZMQInteractiveShell.quiet = False + +# See also: InteractiveShell.separate_in +# c.ZMQInteractiveShell.separate_in = '\n' + +# See also: InteractiveShell.separate_out +# c.ZMQInteractiveShell.separate_out = '' + +# See also: InteractiveShell.separate_out2 +# c.ZMQInteractiveShell.separate_out2 = '' + +## Show rewritten input, e.g. for autocall. +# See also: InteractiveShell.show_rewritten_input +# c.ZMQInteractiveShell.show_rewritten_input = True + +## +# See also: InteractiveShell.sphinxify_docstring +# c.ZMQInteractiveShell.sphinxify_docstring = False + +## Warn if running in a virtual environment with no IPython installed (so IPython +# from the global environment is used). +# See also: InteractiveShell.warn_venv +# c.ZMQInteractiveShell.warn_venv = True + +# See also: InteractiveShell.wildcards_case_sensitive +# c.ZMQInteractiveShell.wildcards_case_sensitive = True + +## Switch modes for the IPython exception handlers. +# See also: InteractiveShell.xmode +# c.ZMQInteractiveShell.xmode = 'Context' + +# ------------------------------------------------------------------------------ +# ProfileDir(LoggingConfigurable) configuration +# ------------------------------------------------------------------------------ +## An object to manage the profile directory and its resources. +# +# The profile directory is used by all IPython applications, to manage +# configuration, logging and security. +# +# This object knows how to find, create and manage these directories. This +# should be used by any code that wants to handle profiles. + +## Set the profile location directly. This overrides the logic used by the +# `profile` option. +# Default: '' +# c.ProfileDir.location = '' + +# ------------------------------------------------------------------------------ +# Session(Configurable) configuration +# ------------------------------------------------------------------------------ +## Object for handling serialization and sending of messages. +# +# The Session object handles building messages and sending them +# with ZMQ sockets or ZMQStream objects. Objects can communicate with each +# other over the network via Session objects, and only need to work with the +# dict-based IPython message spec. The Session will handle +# serialization/deserialization, security, and metadata. +# +# Sessions support configurable serialization via packer/unpacker traits, +# and signing with HMAC digests via the key/keyfile traits. +# +# Parameters +# ---------- +# +# debug : bool +# whether to trigger extra debugging statements +# packer/unpacker : str : 'json', 'pickle' or import_string +# importstrings for methods to serialize message parts. If just +# 'json' or 'pickle', predefined JSON and pickle packers will be used. +# Otherwise, the entire importstring must be used. +# +# The functions must accept at least valid JSON input, and output +# *bytes*. +# +# For example, to use msgpack: +# packer = 'msgpack.packb', unpacker='msgpack.unpackb' +# pack/unpack : callables +# You can also set the pack/unpack callables for serialization directly. +# session : bytes +# the ID of this Session object. The default is to generate a new UUID. +# username : unicode +# username added to message headers. The default is to ask the OS. +# key : bytes +# The key used to initialize an HMAC signature. If unset, messages +# will not be signed or checked. +# keyfile : filepath +# The file containing a key. If this is set, `key` will be initialized +# to the contents of the file. + +## Threshold (in bytes) beyond which an object's buffer should be extracted to +# avoid pickling. +# Default: 1024 +# c.Session.buffer_threshold = 1024 + +## Whether to check PID to protect against calls after fork. +# +# This check can be disabled if fork-safety is handled elsewhere. +# Default: True +# c.Session.check_pid = True + +## Threshold (in bytes) beyond which a buffer should be sent without copying. +# Default: 65536 +# c.Session.copy_threshold = 65536 + +## Debug output in the Session +# Default: False +# c.Session.debug = False + +## The maximum number of digests to remember. +# +# The digest history will be culled when it exceeds this value. +# Default: 65536 +# c.Session.digest_history_size = 65536 + +## The maximum number of items for a container to be introspected for custom serialization. +# Containers larger than this are pickled outright. +# Default: 64 +# c.Session.item_threshold = 64 + +## execution key, for signing messages. +# Default: b'' +# c.Session.key = b'' + +## path to file containing execution key. +# Default: '' +# c.Session.keyfile = '' + +## Metadata dictionary, which serves as the default top-level metadata dict for +# each message. +# Default: {} +# c.Session.metadata = {} + +## The name of the packer for serializing messages. +# Should be one of 'json', 'pickle', or an import name +# for a custom callable serializer. +# Default: 'json' +# c.Session.packer = 'json' + +## The UUID identifying this session. +# Default: '' +# c.Session.session = '' + +## The digest scheme used to construct the message signatures. +# Must have the form 'hmac-HASH'. +# Default: 'hmac-sha256' +# c.Session.signature_scheme = 'hmac-sha256' + +## The name of the unpacker for unserializing messages. +# Only used with custom functions for `packer`. +# Default: 'json' +# c.Session.unpacker = 'json' + +## Username for the Session. Default is your system username. +# Default: 'user' +# c.Session.username = 'user' + +# Truncate large collections (lists, dicts, tuples, sets) to this size. +# Set to 0 to disable truncation. +# Default: 1000 +c.PlainTextFormatter.max_seq_length = 0 diff --git a/packages/code-interpreter-template/jupyter-healthcheck.sh b/packages/code-interpreter-template/jupyter-healthcheck.sh new file mode 100644 index 0000000000..e604a06c4b --- /dev/null +++ b/packages/code-interpreter-template/jupyter-healthcheck.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Custom health check for Jupyter Server +# Verifies the server is responsive via the /api/status endpoint + +MAX_RETRIES=50 +RETRY_INTERVAL=0.2 + +for i in $(seq 1 $MAX_RETRIES); do + status_code=$(curl --unix-socket /run/e2b-jupyter/server.sock -s -o /dev/null -w "%{http_code}" "http://localhost/api/status") + + if [ "$status_code" -eq 200 ]; then + echo "Jupyter Server is healthy" + exit 0 + fi + + if [ $((i % 10)) -eq 0 ]; then + echo "Waiting for Jupyter Server to become healthy... (attempt $i/$MAX_RETRIES)" + fi + sleep $RETRY_INTERVAL +done + +echo "Jupyter Server health check failed after $MAX_RETRIES attempts" +exit 1 diff --git a/packages/code-interpreter-template/jupyter_server_config.py b/packages/code-interpreter-template/jupyter_server_config.py new file mode 100644 index 0000000000..4d6985dc1f --- /dev/null +++ b/packages/code-interpreter-template/jupyter_server_config.py @@ -0,0 +1,67 @@ +# Configuration file for jupyter-server. + +c = get_config() # noqa + +c.ServerApp.sock = "/run/e2b-jupyter/server.sock" +c.ServerApp.sock_mode = "0600" + + +# Pin the contents root directory. +# +# Sessions are created with a relative path (a bare uuid, see +# server/contexts.py). Without an explicit root_dir, jupyter-server +# inherits the process working directory as its root — which is "/" +# under systemd (jupyter.service has no WorkingDirectory). Since +# jupyter-server 2.18.0 (CVE-2026-35397 path-traversal hardening), a +# root_dir of "/" makes every POST /api/sessions fail with +# " is outside root contents directory", so the server never +# starts. Pinning it to /home/user matches the execution cwd. +c.ServerApp.root_dir = "/home/user" + + +# Set the Access-Control-Allow-Origin header +# +# Use '*' to allow any origin to access your server. +# +# Takes precedence over allow_origin_pat. +# Default: '' +c.ServerApp.allow_origin = "" + + +# Allow requests where the Host header doesn't point to a local server +# +# By default, requests get a 403 forbidden response if the 'Host' header +# shows that the browser thinks it's on a non-local domain. +# Setting this option to True disables this check. +# +# This protects against 'DNS rebinding' attacks, where a remote web server +# serves you a page and then changes its DNS to send later requests to a +# local IP, bypassing same-origin checks. +# +# Local IP addresses (such as 127.0.0.1 and ::1) are allowed as local, +# along with hostnames configured in local_hostnames. +# Default: False +c.ServerApp.allow_remote_access = False + +# Disable cross-site-request-forgery protection +# +# Jupyter server includes protection from cross-site request forgeries, +# requiring API requests to either: +# +# - originate from pages served by this server (validated with XSRF cookie and token), or +# - authenticate with a token +# +# Some anonymous compute resources still desire the ability to run code, +# completely without authentication. +# These services can disable all authentication and security checks, +# with the full knowledge of what that implies. +# Default: False +c.ServerApp.disable_check_xsrf = True + +# Whether to allow the user to run the server as root. +# Default: False +c.ServerApp.allow_root = True + +# (bytes/sec) Maximum rate at which messages can be sent on iopub before they are limited. +# Default: 1000000 +c.ServerApp.iopub_data_rate_limit = 1000000000 diff --git a/packages/code-interpreter-template/matplotlibrc b/packages/code-interpreter-template/matplotlibrc new file mode 100644 index 0000000000..b71d01f3a4 --- /dev/null +++ b/packages/code-interpreter-template/matplotlibrc @@ -0,0 +1 @@ +font.family: sans-serif, Noto Sans CJK JP diff --git a/packages/code-interpreter-template/package.json b/packages/code-interpreter-template/package.json new file mode 100644 index 0000000000..ca49503dbc --- /dev/null +++ b/packages/code-interpreter-template/package.json @@ -0,0 +1,9 @@ +{ + "name": "@e2b/code-interpreter-template", + "private": true, + "version": "0.4.6", + "scripts": { + "lint": "uvx ruff@0.11.12 check .", + "format": "uvx ruff@0.11.12 format ." + } +} diff --git a/packages/code-interpreter-template/requirements-dev.txt b/packages/code-interpreter-template/requirements-dev.txt new file mode 100644 index 0000000000..fa0feda2d0 --- /dev/null +++ b/packages/code-interpreter-template/requirements-dev.txt @@ -0,0 +1,2 @@ +e2b==2.32.0 +python-dotenv==1.2.2 \ No newline at end of file diff --git a/packages/code-interpreter-template/requirements.txt b/packages/code-interpreter-template/requirements.txt new file mode 100644 index 0000000000..3e453aa16c --- /dev/null +++ b/packages/code-interpreter-template/requirements.txt @@ -0,0 +1,44 @@ +# Jupyter server requirements +jupyter-server==2.20.0 +ipykernel==6.31.0 +ipython==9.15.0 + +orjson==3.11.9 +pandas==2.2.3 +matplotlib==3.10.9 +pillow==12.3.0 + +# Latest version for +e2b_charts + +# Other packages +aiohttp==3.14.3 +beautifulsoup4==4.15.0 +bokeh==3.9.1 +gensim==4.4.0 +imageio==2.37.3 +joblib==1.5.3 +librosa==0.11.0 +nltk==3.10.3 +numpy==2.3.5 +numba==0.66.0 +opencv-python==4.11.0.86 +openpyxl==3.1.5 +plotly==6.0.1 +kaleido==1.3.0 +pytest==9.0.3 +python-docx==1.1.2 +pytz==2025.2 +requests==2.33.0 +scikit-image==0.25.2 +scikit-learn==1.6.1 +scipy==1.17.1 +seaborn==0.13.2 +soundfile==0.13.1 +spacy==3.8.14 +textblob==0.19.0 +tornado==6.5.8 +urllib3==2.7.0 +xarray==2025.4.0 +xlrd==2.0.2 +sympy==1.14.0 diff --git a/packages/code-interpreter-template/server/api/models/__init__.py b/packages/code-interpreter-template/server/api/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/code-interpreter-template/server/api/models/context.py b/packages/code-interpreter-template/server/api/models/context.py new file mode 100644 index 0000000000..5efb850e81 --- /dev/null +++ b/packages/code-interpreter-template/server/api/models/context.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel, StrictStr +from pydantic import Field + + +class Context(BaseModel): + id: StrictStr = Field(description="Context ID") + language: StrictStr = Field(description="Language of the context") + cwd: StrictStr = Field(description="Current working directory of the context") + + def __hash__(self): + return hash(self.id) diff --git a/packages/code-interpreter-template/server/api/models/create_context.py b/packages/code-interpreter-template/server/api/models/create_context.py new file mode 100644 index 0000000000..1e1fefb736 --- /dev/null +++ b/packages/code-interpreter-template/server/api/models/create_context.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel, StrictStr +from pydantic import Field +from typing import Optional + + +class CreateContext(BaseModel): + cwd: Optional[StrictStr] = Field( + default="/home/user", + description="Current working directory", + ) + language: Optional[StrictStr] = Field( + default="python", description="Language of the context" + ) diff --git a/packages/code-interpreter-template/server/api/models/env_vars.py b/packages/code-interpreter-template/server/api/models/env_vars.py new file mode 100644 index 0000000000..7c51bcca90 --- /dev/null +++ b/packages/code-interpreter-template/server/api/models/env_vars.py @@ -0,0 +1,4 @@ +from typing import Dict +from pydantic import StrictStr + +EnvVars = Dict[StrictStr, str] diff --git a/packages/code-interpreter-template/server/api/models/error.py b/packages/code-interpreter-template/server/api/models/error.py new file mode 100644 index 0000000000..b17a6abc13 --- /dev/null +++ b/packages/code-interpreter-template/server/api/models/error.py @@ -0,0 +1,17 @@ +from typing import Optional +from pydantic import BaseModel, StrictStr +from pydantic import Field + +from api.models.output import OutputType + + +class Error(BaseModel): + type: OutputType = OutputType.ERROR + + name: Optional[StrictStr] = Field(default=None, description="Name of the exception") + value: Optional[StrictStr] = Field( + default=None, description="Value of the exception" + ) + traceback: Optional[StrictStr] = Field( + default=None, description="Traceback of the exception" + ) diff --git a/packages/code-interpreter-template/server/api/models/execution_request.py b/packages/code-interpreter-template/server/api/models/execution_request.py new file mode 100644 index 0000000000..bb691e9eff --- /dev/null +++ b/packages/code-interpreter-template/server/api/models/execution_request.py @@ -0,0 +1,16 @@ +from typing import Optional +from pydantic import BaseModel, StrictStr +from pydantic import Field + +from .env_vars import EnvVars + + +class ExecutionRequest(BaseModel): + code: StrictStr = Field(description="Code to be executed") + context_id: Optional[StrictStr] = Field(default=None, description="Context ID") + language: Optional[StrictStr] = Field( + default=None, description="Language of the code" + ) + env_vars: Optional[EnvVars] = Field( + description="Environment variables", default=None + ) diff --git a/packages/code-interpreter-template/server/api/models/logs.py b/packages/code-interpreter-template/server/api/models/logs.py new file mode 100644 index 0000000000..d9a433fefa --- /dev/null +++ b/packages/code-interpreter-template/server/api/models/logs.py @@ -0,0 +1,19 @@ +import datetime + + +from typing import Optional +from pydantic import BaseModel, StrictStr + +from api.models.output import OutputType + + +class Stdout(BaseModel): + type: OutputType = OutputType.STDOUT + text: Optional[StrictStr] = None + timestamp: Optional[datetime.datetime] = None + + +class Stderr(BaseModel): + type: OutputType = OutputType.STDERR + text: Optional[StrictStr] = None + timestamp: Optional[datetime.datetime] = None diff --git a/packages/code-interpreter-template/server/api/models/output.py b/packages/code-interpreter-template/server/api/models/output.py new file mode 100644 index 0000000000..8e620c3816 --- /dev/null +++ b/packages/code-interpreter-template/server/api/models/output.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel + + +class OutputType(Enum): + """ + Represents the type of the data to send to the client. + """ + + STDOUT = "stdout" + STDERR = "stderr" + RESULT = "result" + ERROR = "error" + NUMBER_OF_EXECUTIONS = "number_of_executions" + END_OF_EXECUTION = "end_of_execution" + UNEXPECTED_END_OF_EXECUTION = "unexpected_end_of_execution" + + +class EndOfExecution(BaseModel): + type: OutputType = OutputType.END_OF_EXECUTION + + +class UnexpectedEndOfExecution(BaseModel): + type: OutputType = OutputType.UNEXPECTED_END_OF_EXECUTION + + +class NumberOfExecutions(BaseModel): + type: OutputType = OutputType.NUMBER_OF_EXECUTIONS + execution_count: int diff --git a/packages/code-interpreter-template/server/api/models/result.py b/packages/code-interpreter-template/server/api/models/result.py new file mode 100644 index 0000000000..90e24a9ac9 --- /dev/null +++ b/packages/code-interpreter-template/server/api/models/result.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import warnings + +from typing import Optional, Iterable +from pydantic import BaseModel + +from api.models.output import OutputType + +warnings.filterwarnings("ignore", category=UserWarning) + + +class Result(BaseModel): + """ + Represents the data to be displayed as a result of executing a cell in a Jupyter notebook. + The result is similar to the structure returned by ipython kernel: https://ipython.readthedocs.io/en/stable/development/execution.html#execution-semantics + + The result can contain multiple types of data, such as text, images, plots, etc. Each type of data is represented + as a string, and the result can contain multiple types of data. The display calls don't have to have text representation, + for the actual result the representation is always present for the result, the other representations are always optional. + """ + + type: OutputType = OutputType.RESULT + + text: Optional[str] = None + html: Optional[str] = None + markdown: Optional[str] = None + svg: Optional[str] = None + png: Optional[str] = None + jpeg: Optional[str] = None + pdf: Optional[str] = None + latex: Optional[str] = None + json: Optional[dict] = None + javascript: Optional[str] = None + data: Optional[dict] = None + chart: Optional[dict] = None + extra: Optional[dict] = None + "Extra data that can be included. Not part of the standard types." + + is_main_result: Optional[bool] = None + "Whether this data is the result of the execetution. Data can be produced by display calls of which can be multiple in a cell." + + def __init__(self, is_main_result: bool, data: [str, str]): + super().__init__() + self.is_main_result = is_main_result + + self.text = data.pop("text/plain", None) + if self.text and ( + (self.text.startswith("'") and self.text.endswith("'")) + or (self.text.startswith('"') and self.text.endswith('"')) + ): + self.text = self.text[1:-1] + + self.html = data.pop("text/html", None) + self.markdown = data.pop("text/markdown", None) + self.svg = data.pop("image/svg+xml", None) + self.png = data.pop("image/png", None) + self.jpeg = data.pop("image/jpeg", None) + self.pdf = data.pop("application/pdf", None) + self.latex = data.pop("text/latex", None) + self.json = data.pop("application/json", None) + self.javascript = data.pop("application/javascript", None) + self.data = data.pop("e2b/data", None) + self.chart = data.pop("e2b/chart", None) + self.extra = data + + def formats(self) -> Iterable[str]: + formats = [] + + for key in [ + "text", + "html", + "markdown", + "svg", + "png", + "jpeg", + "pdf", + "latex", + "json", + "javascript", + "data", + "chart", + ]: + if getattr(self, key): + formats.append(key) + + if self.extra: + for key in self.extra: + formats.append(key) + + return formats + + def __str__(self) -> str: + """ + Returns the text representation of the data. + + :return: The text representation of the data. + """ + return self.__repr__() + + def __repr__(self) -> str: + if self.text: + return f"Result({self.text})" + formats = self.formats() + return f"Result with formats: {formats}" diff --git a/packages/code-interpreter-template/server/consts.py b/packages/code-interpreter-template/server/consts.py new file mode 100644 index 0000000000..c861dc78e0 --- /dev/null +++ b/packages/code-interpreter-template/server/consts.py @@ -0,0 +1,2 @@ +JUPYTER_SOCKET_PATH = "/run/e2b-jupyter/server.sock" +JUPYTER_BASE_URL = "http://localhost" diff --git a/packages/code-interpreter-template/server/contexts.py b/packages/code-interpreter-template/server/contexts.py new file mode 100644 index 0000000000..773f3fa826 --- /dev/null +++ b/packages/code-interpreter-template/server/contexts.py @@ -0,0 +1,72 @@ +import logging +import uuid +from typing import Optional + +from api.models.context import Context +from fastapi.responses import PlainTextResponse + +from consts import JUPYTER_BASE_URL +from errors import ExecutionError +from messaging import ContextWebSocket + +logger = logging.Logger(__name__) + + +def get_kernel_for_language(language: str) -> str: + if language == "typescript": + return "javascript" + + return language + + +def normalize_language(language: Optional[str]) -> str: + if not language: + return "python" + + language = language.lower().strip() + + if language == "js": + return "javascript" + + if language == "ts": + return "typescript" + + return language + + +async def create_context(client, websockets: dict, language: str, cwd: str) -> Context: + data = { + "path": str(uuid.uuid4()), + "kernel": {"name": get_kernel_for_language(language)}, + "type": "notebook", + "name": str(uuid.uuid4()), + } + logger.debug(f"Creating new {language} context") + + response = await client.post(f"{JUPYTER_BASE_URL}/api/sessions", json=data) + + if not response.is_success: + raise Exception( + f"Failed to create context: {response.text}", + ) + + session_data = response.json() + session_id = session_data["id"] + context_id = session_data["kernel"]["id"] + + logger.debug(f"Created context {context_id}") + + ws = ContextWebSocket(context_id, session_id, language, cwd) + await ws.connect() + websockets[context_id] = ws + + logger.info(f"Setting working directory to {cwd}") + try: + await ws.change_current_directory(cwd, language) + except ExecutionError: + return PlainTextResponse( + "Failed to set working directory", + status_code=500, + ) + + return Context(language=language, id=context_id, cwd=cwd) diff --git a/packages/code-interpreter-template/server/envs.py b/packages/code-interpreter-template/server/envs.py new file mode 100644 index 0000000000..8752e7b97b --- /dev/null +++ b/packages/code-interpreter-template/server/envs.py @@ -0,0 +1,20 @@ +import os +from typing import Optional + +import httpx + +LOCAL = os.getenv("E2B_LOCAL", False) +ENVD_PORT = 49983 + + +async def get_envs(access_token: Optional[str]) -> dict: + if LOCAL: + return {"E2B_TEST_VARIABLE": "true"} + async with httpx.AsyncClient() as client: + headers = {} + if access_token: + headers["X-Access-Token"] = f"{access_token}" + response = await client.get( + f"http://localhost:{ENVD_PORT}/envs", headers=headers + ) + return response.json() diff --git a/packages/code-interpreter-template/server/errors.py b/packages/code-interpreter-template/server/errors.py new file mode 100644 index 0000000000..ab4b43bcba --- /dev/null +++ b/packages/code-interpreter-template/server/errors.py @@ -0,0 +1,2 @@ +class ExecutionError(Exception): + pass diff --git a/packages/code-interpreter-template/server/main.py b/packages/code-interpreter-template/server/main.py new file mode 100644 index 0000000000..a8d43b7430 --- /dev/null +++ b/packages/code-interpreter-template/server/main.py @@ -0,0 +1,219 @@ +import logging +import sys +import httpx + +from typing import Dict, Union, Literal, List + +from contextlib import asynccontextmanager +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import PlainTextResponse + +from api.models.context import Context +from api.models.create_context import CreateContext +from api.models.execution_request import ExecutionRequest +from consts import JUPYTER_BASE_URL, JUPYTER_SOCKET_PATH +from contexts import create_context, normalize_language +from messaging import ContextWebSocket +from stream import StreamingListJsonResponse +from utils.locks import LockedMap + +logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) +logger = logging.Logger(__name__) +http_logger = logging.getLogger("httpcore.http11") +http_logger.setLevel(logging.WARNING) + + +websockets: Dict[Union[str, Literal["default"]], ContextWebSocket] = {} +default_websockets = LockedMap() +global client + + +@asynccontextmanager +async def lifespan(app: FastAPI): + global client + client = httpx.AsyncClient( + transport=httpx.AsyncHTTPTransport(uds=JUPYTER_SOCKET_PATH), trust_env=False + ) + + try: + python_context = await create_context( + client, websockets, "python", "/home/user" + ) + default_websockets["python"] = python_context.id + websockets["default"] = websockets[python_context.id] + + javascript_context = await create_context( + client, websockets, "javascript", "/home/user" + ) + default_websockets["javascript"] = javascript_context.id + + logger.info("Connected to default runtime") + yield + + # Will cleanup after application shuts down + for ws in websockets.values(): + await ws.close() + + await client.aclose() + except Exception as e: + logger.error(f"Failed to initialize default context: {e}") + raise + + +app = FastAPI(lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +logger.info("Starting Code Interpreter server") + + +@app.get("/health") +async def get_health(): + return "OK" + + +@app.post("/execute") +async def post_execute(request: Request, exec_request: ExecutionRequest): + logger.info(f"Executing code: {exec_request.code}") + + if exec_request.context_id and exec_request.language: + return PlainTextResponse( + "Only one of context_id or language can be provided", + status_code=400, + ) + + context_id = None + if exec_request.language: + language = normalize_language(exec_request.language) + + async with await default_websockets.get_lock(language): + context_id = default_websockets.get(language) + + if not context_id: + try: + context = await create_context( + client, websockets, language, "/home/user" + ) + except Exception as e: + return PlainTextResponse(str(e), status_code=500) + + context_id = context.id + default_websockets[language] = context_id + + elif exec_request.context_id: + context_id = exec_request.context_id + + if context_id: + ws = websockets.get(context_id, None) + else: + ws = websockets["default"] + + if not ws: + return PlainTextResponse( + f"Context {exec_request.context_id} not found", + status_code=404, + ) + + return StreamingListJsonResponse( + ws.execute( + exec_request.code, + env_vars=exec_request.env_vars, + access_token=request.headers.get("X-Access-Token", None), + ) + ) + + +@app.post("/contexts") +async def post_contexts(request: CreateContext) -> Context: + logger.info("Creating a new context") + + language = normalize_language(request.language) + cwd = request.cwd or "/home/user" + + try: + return await create_context(client, websockets, language, cwd) + except Exception as e: + return PlainTextResponse(str(e), status_code=500) + + +@app.get("/contexts") +async def get_contexts() -> List[Context]: + logger.info("Listing contexts") + + return [ + Context( + id=ws.context_id, + language=ws.language, + cwd=ws.cwd, + ) + for key, ws in websockets.items() + if key != "default" + ] + + +@app.post("/contexts/{context_id}/restart") +async def restart_context(context_id: str) -> None: + logger.info(f"Restarting context {context_id}") + + ws = websockets.get(context_id, None) + if not ws: + return PlainTextResponse( + f"Context {context_id} not found", + status_code=404, + ) + + session_id = ws.session_id + + await ws.close() + + response = await client.post( + f"{JUPYTER_BASE_URL}/api/kernels/{ws.context_id}/restart" + ) + if not response.is_success: + return PlainTextResponse( + f"Failed to restart context {context_id}", + status_code=500, + ) + + ws = ContextWebSocket( + ws.context_id, + session_id, + ws.language, + ws.cwd, + ) + + await ws.connect() + + websockets[context_id] = ws + + +@app.delete("/contexts/{context_id}") +async def remove_context(context_id: str) -> None: + logger.info(f"Removing context {context_id}") + + ws = websockets.get(context_id, None) + if not ws: + return PlainTextResponse( + f"Context {context_id} not found", + status_code=404, + ) + + try: + await ws.close() + except: # noqa: E722 + pass + + response = await client.delete(f"{JUPYTER_BASE_URL}/api/kernels/{ws.context_id}") + if not response.is_success: + return PlainTextResponse( + f"Failed to remove context {context_id}", + status_code=500, + ) + + del websockets[context_id] diff --git a/packages/code-interpreter-template/server/messaging.py b/packages/code-interpreter-template/server/messaging.py new file mode 100644 index 0000000000..7248a5b46e --- /dev/null +++ b/packages/code-interpreter-template/server/messaging.py @@ -0,0 +1,612 @@ +import datetime +import json +import logging +import uuid +import asyncio + +import httpx + +from asyncio import Queue +from typing import ( + Dict, + Optional, + Union, +) +from pydantic import StrictStr +from websockets.client import WebSocketClientProtocol, unix_connect +from websockets.exceptions import ( + ConnectionClosedError, + WebSocketException, +) + +from api.models.error import Error +from api.models.logs import Stdout, Stderr +from api.models.result import Result +from api.models.output import ( + EndOfExecution, + NumberOfExecutions, + OutputType, + UnexpectedEndOfExecution, +) +from consts import JUPYTER_BASE_URL, JUPYTER_SOCKET_PATH +from errors import ExecutionError +from envs import get_envs + +logger = logging.getLogger(__name__) + +MAX_RECONNECT_RETRIES = 3 +PING_TIMEOUT = 30 +KEEPALIVE_INTERVAL = 5 # seconds between keepalive pings during streaming + + +class Execution: + def __init__(self, in_background: bool = False): + self.queue = Queue[ + Union[ + Result, + Error, + Stdout, + Stderr, + EndOfExecution, + NumberOfExecutions, + UnexpectedEndOfExecution, + ] + ]() + self.input_accepted = False + self.errored = False + self.in_background = in_background + + +class ContextWebSocket: + _ws: Optional[WebSocketClientProtocol] = None + _receive_task: Optional[asyncio.Task] = None + _global_env_vars: Optional[Dict[StrictStr, str]] = None + _cleanup_task: Optional[asyncio.Task] = None + + def __init__(self, context_id: str, session_id: str, language: str, cwd: str): + self.language = language + self.cwd = cwd + self.context_id = context_id + self.url = f"ws://localhost/api/kernels/{context_id}/channels" + self.session_id = session_id + self._executions: Dict[str, Execution] = {} + self._lock = asyncio.Lock() + + async def reconnect(self): + if self._ws is not None: + await self._ws.close(reason="Reconnecting") + + if self._receive_task is not None: + await self._receive_task + + await self.connect() + + async def connect(self): + logger.debug(f"WebSocket connecting to {self.url}") + + ws_logger = logger.getChild("websockets.client") + ws_logger.setLevel(logging.ERROR) + + self._ws = await unix_connect( + JUPYTER_SOCKET_PATH, + uri=self.url, + ping_timeout=PING_TIMEOUT, + max_size=None, + max_queue=None, + logger=ws_logger, + ) + + logger.info(f"WebSocket connected to {self.url}") + self._receive_task = asyncio.create_task( + self._receive_message(), + name="receive_message", + ) + + async def interrupt(self): + """Interrupt the current kernel execution via the Jupyter REST API.""" + try: + async with httpx.AsyncClient( + transport=httpx.AsyncHTTPTransport(uds=JUPYTER_SOCKET_PATH), + trust_env=False, + ) as client: + response = await client.post( + f"{JUPYTER_BASE_URL}/api/kernels/{self.context_id}/interrupt" + ) + if response.is_success: + logger.info(f"Kernel {self.context_id} interrupted successfully") + else: + logger.error( + f"Failed to interrupt kernel {self.context_id}: {response.status_code}" + ) + except Exception as e: + logger.error(f"Error interrupting kernel {self.context_id}: {e}") + + def _get_execute_request( + self, msg_id: str, code: Union[str, StrictStr], background: bool + ) -> str: + return json.dumps( + { + "header": { + "msg_id": msg_id, + "username": "e2b", + "session": self.session_id, + "msg_type": "execute_request", + "version": "5.3", + "date": datetime.datetime.now(datetime.timezone.utc).isoformat(), + }, + "parent_header": {}, + "metadata": { + "trusted": True, + "deletedCells": [], + "recordTiming": False, + "cellId": str(uuid.uuid4()), + }, + "content": { + "code": code, + "silent": background, + "store_history": True, + "user_expressions": {}, + "stop_on_error": True, + "allow_stdin": False, + }, + } + ) + + def _set_env_var_snippet(self, key: str, value: str) -> str: + """Get environment variable set command for the current language.""" + if self.language == "python": + return f"import os; os.environ['{key}'] = '{value}'" + elif self.language in ["javascript", "typescript"]: + return f"process.env['{key}'] = '{value}'" + elif self.language == "r": + return f'Sys.setenv({key} = "{value}")' + elif self.language == "java": + return f'System.setProperty("{key}", "{value}");' + elif self.language == "bash": + return f"export {key}='{value}'" + return "" + + def _delete_env_var_snippet(self, key: str) -> str: + """Get environment variable delete command for the current language.""" + if self.language == "python": + return f"import os; del os.environ['{key}']" + elif self.language in ["javascript", "typescript"]: + return f"delete process.env['{key}']" + elif self.language == "r": + return f"Sys.unsetenv('{key}')" + elif self.language == "java": + return f'System.clearProperty("{key}");' + elif self.language == "bash": + return f"unset {key}" + return "" + + def _set_env_vars_code(self, env_vars: Dict[StrictStr, str]) -> str: + """Build environment variable code for the current language.""" + env_commands = [] + for k, v in env_vars.items(): + command = self._set_env_var_snippet(k, v) + if command: + env_commands.append(command) + + return "\n".join(env_commands) + + def _reset_env_vars_code(self, env_vars: Dict[StrictStr, str]) -> str: + """Build environment variable cleanup code for the current language.""" + cleanup_commands = [] + + for key in env_vars: + # Check if this var exists in global env vars + if self._global_env_vars and key in self._global_env_vars: + # Reset to global value + value = self._global_env_vars[key] + command = self._set_env_var_snippet(key, value) + else: + # Remove the variable + command = self._delete_env_var_snippet(key) + + if command: + cleanup_commands.append(command) + + return "\n".join(cleanup_commands) + + def _get_code_indentation(self, code: str) -> str: + """Get the indentation from the first non-empty line of code.""" + if not code or not code.strip(): + return "" + + lines = code.split("\n") + for line in lines: + if line.strip(): # First non-empty line + return line[: len(line) - len(line.lstrip())] + + return "" + + def _indent_code_with_level(self, code: str, indent_level: str) -> str: + """Apply the given indentation level to each line of code.""" + if not code or not indent_level: + return code + + lines = code.split("\n") + indented_lines = [] + + for line in lines: + if line.strip(): # Non-empty lines + indented_lines.append(indent_level + line) + else: + indented_lines.append(line) + + return "\n".join(indented_lines) + + async def _cleanup_env_vars(self, env_vars: Dict[StrictStr, str]): + """Clean up environment variables in a separate execution request.""" + message_id = str(uuid.uuid4()) + self._executions[message_id] = Execution(in_background=True) + + try: + cleanup_code = self._reset_env_vars_code(env_vars) + if cleanup_code: + logger.info(f"Cleaning up env vars: {cleanup_code}") + request = self._get_execute_request(message_id, cleanup_code, True) + await self._ws.send(request) + + async for item in self._wait_for_result(message_id): + if item["type"] == "error": + logger.error(f"Error during env var cleanup: {item}") + finally: + del self._executions[message_id] + + async def _wait_for_result(self, message_id: str): + queue = self._executions[message_id].queue + + # Use a timeout on queue.get() to periodically send keepalives. + # Without keepalives, the generator blocks indefinitely waiting for + # kernel output. If the client silently disappears (e.g. network + # failure), uvicorn can only detect the broken connection when it + # tries to write — so we force a write every KEEPALIVE_INTERVAL + # seconds. This ensures timely disconnect detection and kernel + # interrupt for abandoned executions (see #213). + while True: + try: + output = await asyncio.wait_for(queue.get(), timeout=KEEPALIVE_INTERVAL) + except asyncio.TimeoutError: + # Yield a keepalive so Starlette writes to the socket. + # If the client has disconnected, the write fails and + # uvicorn delivers http.disconnect, which cancels this + # generator via CancelledError. + yield {"type": "keepalive"} + continue + + if output.type == OutputType.END_OF_EXECUTION: + break + + if output.type == OutputType.UNEXPECTED_END_OF_EXECUTION: + logger.error(f"Unexpected end of execution for code ({message_id})") + yield Error( + name="UnexpectedEndOfExecution", + value="Connection to the execution was closed before the execution was finished", + traceback="", + ) + break + + yield output.model_dump(exclude_none=True) + + async def change_current_directory( + self, path: Union[str, StrictStr], language: str + ): + message_id = str(uuid.uuid4()) + self._executions[message_id] = Execution(in_background=True) + if language == "python": + request = self._get_execute_request(message_id, f"%cd {path}", True) + elif language in ("javascript", "typescript"): + request = self._get_execute_request( + message_id, f"process.chdir('{path}')", True + ) + elif language == "r": + request = self._get_execute_request(message_id, f"setwd('{path}')", True) + # This does not actually change the working directory, but sets the user.dir property + elif language == "java": + request = self._get_execute_request( + message_id, f'System.setProperty("user.dir", "{path}");', True + ) + elif language == "bash": + request = self._get_execute_request(message_id, f"cd '{path}'", True) + else: + return + + await self._ws.send(request) + + async for item in self._wait_for_result(message_id): + if item["type"] == "error": + raise ExecutionError(f"Error during execution: {item}") + + async def execute( + self, + code: Union[str, StrictStr], + env_vars: Dict[StrictStr, str], + access_token: str, + ): + if self._ws is None: + raise Exception("WebSocket not connected") + + async with self._lock: + # Wait for any pending cleanup task to complete + if self._cleanup_task and not self._cleanup_task.done(): + logger.debug("Waiting for pending cleanup task to complete") + try: + await self._cleanup_task + except Exception as e: + logger.warning(f"Cleanup task failed: {e}") + finally: + self._cleanup_task = None + + # Get the indentation level from the code + code_indent = self._get_code_indentation(code) + + # Build the complete code snippet with env vars + complete_code = code + + global_env_vars_snippet = "" + env_vars_snippet = "" + + if self._global_env_vars is None: + self._global_env_vars = await get_envs(access_token=access_token) + global_env_vars_snippet = self._set_env_vars_code(self._global_env_vars) + + if env_vars: + env_vars_snippet = self._set_env_vars_code(env_vars) + + if global_env_vars_snippet or env_vars_snippet: + indented_env_code = self._indent_code_with_level( + f"{global_env_vars_snippet}\n{env_vars_snippet}", code_indent + ) + complete_code = f"{indented_env_code}\n{complete_code}" + + message_id = str(uuid.uuid4()) + execution = Execution() + self._executions[message_id] = execution + + # Send the code for execution + # Initial request and retries + for i in range(1 + MAX_RECONNECT_RETRIES): + try: + logger.info( + f"Sending code for the execution ({message_id}): {complete_code}" + ) + request = self._get_execute_request( + message_id, complete_code, False + ) + await self._ws.send(request) + break + except (ConnectionClosedError, WebSocketException) as e: + # Keep the last result, even if error + if i < MAX_RECONNECT_RETRIES: + logger.warning( + f"WebSocket connection lost while sending execution request, {i + 1}. reconnecting...: {str(e)}" + ) + await self.reconnect() + else: + # The retry didn't help, request wasn't sent successfully + logger.error("Failed to send execution request") + await execution.queue.put( + Error( + name="WebSocketError", + value="Failed to send execution request", + traceback="", + ) + ) + await execution.queue.put(UnexpectedEndOfExecution()) + + # Stream the results. + # If the client disconnects (Starlette cancels the task), we + # interrupt the kernel so the next execution isn't blocked (#213). + try: + async for item in self._wait_for_result(message_id): + yield item + except (asyncio.CancelledError, GeneratorExit): + logger.warning( + f"Client disconnected during execution ({message_id}), interrupting kernel" + ) + # Shield the interrupt from the ongoing cancellation so + # the HTTP request to the kernel actually completes. + try: + await asyncio.shield(self.interrupt()) + except asyncio.CancelledError: + pass + raise + finally: + if message_id in self._executions: + del self._executions[message_id] + + # Clean up env vars in a separate request after the main code has run + if env_vars: + self._cleanup_task = asyncio.create_task( + self._cleanup_env_vars(env_vars) + ) + + async def _receive_message(self): + if not self._ws: + logger.error("No WebSocket connection") + return + + try: + async for message in self._ws: + await self._process_message(json.loads(message)) + except Exception as e: + logger.error(f"WebSocket received error while receiving messages: {str(e)}") + finally: + # To prevent infinite hang, we need to cancel all ongoing execution as we could lost results during the reconnect + # Thanks to the locking, there can be either no ongoing execution or just one. + for key, execution in self._executions.items(): + await execution.queue.put( + Error( + name="WebSocketError", + value="The connections was lost, rerun the code to get the results", + traceback="", + ) + ) + await execution.queue.put(UnexpectedEndOfExecution()) + + async def _process_message(self, data: dict): + """ + Process messages from the WebSocket + + Message types: + https://jupyter-client.readthedocs.io/en/stable/messaging.html + + :param data: The message data + """ + if ( + data["msg_type"] == "status" + and data["content"]["execution_state"] == "restarting" + ): + logger.error("Context is restarting") + for execution in self._executions.values(): + await execution.queue.put( + Error( + name="ContextRestarting", + value="Context was restarted", + traceback="", + ) + ) + await execution.queue.put(EndOfExecution()) + return + + parent_msg_ig = data["parent_header"].get("msg_id", None) + if parent_msg_ig is None: + logger.warning("Parent message ID not found. %s", data) + return + + execution = self._executions.get(parent_msg_ig) + if not execution: + return + + queue = execution.queue + if data["msg_type"] == "error": + logger.debug( + f"Execution {parent_msg_ig} finished execution with error: {data['content']['ename']}: {data['content']['evalue']}" + ) + + if execution.errored: + return + + execution.errored = True + await queue.put( + Error( + name=data["content"]["ename"], + value=data["content"]["evalue"], + traceback="".join(data["content"]["traceback"]), + ) + ) + + elif data["msg_type"] == "stream": + if data["content"]["name"] == "stdout": + logger.debug( + f"Execution {parent_msg_ig} received stdout: {data['content']['text']}" + ) + await queue.put( + Stdout( + text=data["content"]["text"], timestamp=data["header"]["date"] + ) + ) + + elif data["content"]["name"] == "stderr": + logger.debug( + f"Execution {parent_msg_ig} received stderr: {data['content']['text']}" + ) + await queue.put( + Stderr( + text=data["content"]["text"], timestamp=data["header"]["date"] + ) + ) + + elif data["msg_type"] in "display_data": + result = Result(is_main_result=False, data=data["content"]["data"]) + logger.debug( + f"Execution {parent_msg_ig} received display data with following formats: {result.formats()}" + ) + await queue.put(result) + + elif data["msg_type"] == "execute_result": + result = Result(is_main_result=True, data=data["content"]["data"]) + logger.debug( + f"Execution {parent_msg_ig} received execution result with following formats: {result.formats()}" + ) + await queue.put(result) + + elif data["msg_type"] == "status": + if data["content"]["execution_state"] == "busy" and execution.in_background: + logger.debug(f"Execution {parent_msg_ig} started execution") + execution.input_accepted = True + + if data["content"]["execution_state"] == "idle": + if execution.input_accepted: + logger.debug(f"Execution {parent_msg_ig} finished execution") + await queue.put(EndOfExecution()) + + elif data["content"]["execution_state"] == "error": + logger.debug(f"Execution {parent_msg_ig} finished execution with error") + await queue.put( + Error( + name=data["content"]["ename"], + value=data["content"]["evalue"], + traceback="".join(data["content"]["traceback"]), + ) + ) + await queue.put(EndOfExecution()) + + elif data["msg_type"] == "execute_reply": + if data["content"]["status"] == "error": + logger.debug(f"Execution {parent_msg_ig} finished execution with error") + + if execution.errored: + return + + execution.errored = True + await queue.put( + Error( + name=data["content"].get("ename", ""), + value=data["content"].get("evalue", ""), + traceback="".join(data["content"].get("traceback", [])), + ) + ) + elif data["content"]["status"] == "abort": + logger.debug(f"Execution {parent_msg_ig} was aborted") + await queue.put( + Error( + name="ExecutionAborted", + value="Execution was aborted", + traceback="", + ) + ) + await queue.put(EndOfExecution()) + elif data["content"]["status"] == "ok": + pass + + elif data["msg_type"] == "execute_input": + logger.debug(f"Input accepted for {parent_msg_ig}") + await queue.put( + NumberOfExecutions(execution_count=data["content"]["execution_count"]) + ) + execution.input_accepted = True + else: + logger.warning(f"[UNHANDLED MESSAGE TYPE]: {data['msg_type']}") + + async def close(self): + logger.debug(f"Closing WebSocket {self.context_id}") + + if self._ws is not None: + await self._ws.close() + + if self._receive_task is not None: + self._receive_task.cancel() + + # Cancel any pending cleanup task + if self._cleanup_task and not self._cleanup_task.done(): + self._cleanup_task.cancel() + try: + await self._cleanup_task + except asyncio.CancelledError: + pass + + for execution in self._executions.values(): + execution.queue.put_nowait(UnexpectedEndOfExecution()) diff --git a/packages/code-interpreter-template/server/requirements.txt b/packages/code-interpreter-template/server/requirements.txt new file mode 100644 index 0000000000..b3e44b710a --- /dev/null +++ b/packages/code-interpreter-template/server/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.111.0 +httpx==0.28.1 +websockets==12.0 +uvicorn[standard]==0.30.1 +requests==2.33.0 +pydantic==2.9.1 diff --git a/packages/code-interpreter-template/server/stream.py b/packages/code-interpreter-template/server/stream.py new file mode 100644 index 0000000000..0ef0195b66 --- /dev/null +++ b/packages/code-interpreter-template/server/stream.py @@ -0,0 +1,40 @@ +import json +from typing import Mapping, Optional, AsyncIterable + +from fastapi.encoders import jsonable_encoder +from starlette.background import BackgroundTask +from fastapi.responses import StreamingResponse + + +class StreamingListJsonResponse(StreamingResponse): + """Converts a pydantic model generator into a streaming HTTP Response + that streams a JSON list, one element at a time. + + See https://github.com/tiangolo/fastapi/issues/1978 + """ + + def __init__( + self, + content_generator: AsyncIterable, + status_code: int = 200, + headers: Optional[Mapping[str, str]] = None, + media_type: Optional[str] = None, + background: Optional[BackgroundTask] = None, + ) -> None: + body_iterator = self._encoded_async_generator(content_generator) + + super().__init__( + content=body_iterator, + status_code=status_code, + headers=headers, + media_type=media_type, + background=background, + ) + + async def _encoded_async_generator(self, async_generator: AsyncIterable): + """Converts an asynchronous pydantic model generator + into a streaming JSON list + """ + async for item in async_generator: + yield f"{json.dumps(jsonable_encoder(item))}\n" + yield '{"type": "end_of_execution"}\n' diff --git a/packages/code-interpreter-template/server/utils/locks.py b/packages/code-interpreter-template/server/utils/locks.py new file mode 100644 index 0000000000..f4a5c6ea68 --- /dev/null +++ b/packages/code-interpreter-template/server/utils/locks.py @@ -0,0 +1,18 @@ +import asyncio + + +class LockedMap(dict): + def __init__(self): + super().__init__() + self._map_lock = asyncio.Lock() + self._locks = {} + + async def get_lock(self, key): + await self._map_lock.acquire() + if key not in self._locks: + self._locks[key] = asyncio.Lock() + + lock = self._locks[key] + print(f"Lock acquired for {key}") + self._map_lock.release() + return lock diff --git a/packages/code-interpreter-template/start-up.sh b/packages/code-interpreter-template/start-up.sh new file mode 100755 index 0000000000..0ea0494d62 --- /dev/null +++ b/packages/code-interpreter-template/start-up.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +install -d -m 0700 /run/e2b-jupyter + +function start_code_interpreter() { + /root/.jupyter/jupyter-healthcheck.sh + if [ $? -ne 0 ]; then + echo "Jupyter Server failed to start, aborting." + exit 1 + fi + + cd /root/.server/ + .venv/bin/uvicorn main:app --host 0.0.0.0 --port 49999 --workers 1 --no-access-log --no-use-colors --timeout-keep-alive 640 +} + +echo "Starting Code Interpreter server..." +start_code_interpreter & +MATPLOTLIBRC=/root/.config/matplotlib/.matplotlibrc jupyter server --IdentityProvider.token="" >/dev/null 2>&1 diff --git a/packages/code-interpreter-template/startup_scripts/0002_data.py b/packages/code-interpreter-template/startup_scripts/0002_data.py new file mode 100644 index 0000000000..3e994487a6 --- /dev/null +++ b/packages/code-interpreter-template/startup_scripts/0002_data.py @@ -0,0 +1,85 @@ +import sys + +import IPython +from IPython.core.formatters import BaseFormatter, JSONFormatter +from traitlets.traitlets import Unicode, ObjectName + + +class E2BDataFormatter(BaseFormatter): + format_type = Unicode("e2b/data") + + print_method = ObjectName("_repr_e2b_data_") + _return_type = (dict, str) + + def __call__(self, obj): + # IPython invokes every registered formatter for every displayed + # object. Gate on sys.modules so a non-DataFrame output (e.g. an + # int from `1 + 1`) doesn't pay the pandas import cost — a + # pandas.DataFrame can only exist if the user already imported + # pandas. + pandas = sys.modules.get("pandas") + if pandas is None or not isinstance(obj, pandas.DataFrame): + return super().__call__(obj) + + result = obj.to_dict(orient="list") + for key, value in result.items(): + # Check each column's values + result[key] = [ + v.isoformat() if isinstance(v, pandas.Timestamp) else v for v in value + ] + return result + + +class E2BChartFormatter(BaseFormatter): + format_type = Unicode("e2b/chart") + + print_method = ObjectName("_repr_e2b_chart_") + _return_type = (dict, str) + + def __call__(self, obj): + # Same sys.modules gate as E2BDataFormatter: a matplotlib Figure + # can only exist if the user already imported matplotlib. + if sys.modules.get("matplotlib") is None: + return super().__call__(obj) + + from matplotlib.pyplot import Figure + + if not isinstance(obj, Figure): + return super().__call__(obj) + + from e2b_charts import chart_figure_to_dict + + try: + return chart_figure_to_dict(obj) + except: # noqa: E722 + return {} + + +class E2BJSONFormatter(JSONFormatter): + def __call__(self, obj): + if isinstance(obj, (list, dict)): + try: + import orjson + + return orjson.loads( + orjson.dumps( + obj, option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS + ) + ), {"expanded": True} + except TypeError: + pass + + return super().__call__(obj) + + +ip = IPython.get_ipython() +ip.display_formatter.formatters["e2b/data"] = E2BDataFormatter( + parent=ip.display_formatter +) +ip.display_formatter.formatters["e2b/chart"] = E2BChartFormatter( + parent=ip.display_formatter +) + +ip.display_formatter.formatters["application/json"] = E2BJSONFormatter( + parent=ip.display_formatter +) diff --git a/packages/code-interpreter-template/startup_scripts/0003_images.py b/packages/code-interpreter-template/startup_scripts/0003_images.py new file mode 100644 index 0000000000..4732d77dc6 --- /dev/null +++ b/packages/code-interpreter-template/startup_scripts/0003_images.py @@ -0,0 +1,24 @@ +from typing import Any + +from IPython.core.display_functions import display +from PIL.Image import Image +from PIL.ImageShow import UnixViewer + + +def show_file(self, path: str, **options: Any) -> int: + # To prevent errors from trying to display image without any display + return 0 + + +UnixViewer.show_file = show_file +original_save = Image.save + + +def save(image, fp, format=None, **options): + if isinstance(fp, str): + display(image) + + original_save(image, fp, format, **options) + + +Image.save = save diff --git a/packages/code-interpreter-template/systemd/code-interpreter.service b/packages/code-interpreter-template/systemd/code-interpreter.service new file mode 100644 index 0000000000..04c165b41d --- /dev/null +++ b/packages/code-interpreter-template/systemd/code-interpreter.service @@ -0,0 +1,17 @@ +[Unit] +Description=Code Interpreter Server +Documentation=https://github.com/e2b-dev/code-interpreter +Requires=jupyter.service +After=jupyter.service +PartOf=jupyter.service +StartLimitBurst=0 + +[Service] +Type=simple +WorkingDirectory=/root/.server +ExecStartPre=/root/.jupyter/jupyter-healthcheck.sh +ExecStart=/root/.server/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 49999 --workers 1 --no-access-log --no-use-colors --timeout-keep-alive 640 +Restart=on-failure +RestartSec=1 +StandardOutput=journal +StandardError=journal diff --git a/packages/code-interpreter-template/systemd/jupyter-debug.conf b/packages/code-interpreter-template/systemd/jupyter-debug.conf new file mode 100644 index 0000000000..89377a6f6d --- /dev/null +++ b/packages/code-interpreter-template/systemd/jupyter-debug.conf @@ -0,0 +1,5 @@ +# Debug-only drop-in: route Jupyter's stdout to the journal (the base unit +# sends it to /dev/null) so ServerApp request/error logs are visible via +# `journalctl -u jupyter`. Applied only by `make_template(debug=True)`. +[Service] +StandardOutput=journal diff --git a/packages/code-interpreter-template/systemd/jupyter.service b/packages/code-interpreter-template/systemd/jupyter.service new file mode 100644 index 0000000000..178124bd2b --- /dev/null +++ b/packages/code-interpreter-template/systemd/jupyter.service @@ -0,0 +1,17 @@ +[Unit] +Description=Jupyter Server +Documentation=https://jupyter-server.readthedocs.io +Wants=code-interpreter.service +StartLimitBurst=0 + +[Service] +Type=simple +RuntimeDirectory=e2b-jupyter +RuntimeDirectoryMode=0700 +Environment=MATPLOTLIBRC=/root/.config/matplotlib/.matplotlibrc +ExecStart=/usr/local/bin/jupyter server --IdentityProvider.token="" +ExecStartPost=-/usr/bin/systemctl reset-failed code-interpreter +Restart=on-failure +RestartSec=1 +StandardOutput=null +StandardError=journal diff --git a/packages/code-interpreter-template/template.py b/packages/code-interpreter-template/template.py new file mode 100644 index 0000000000..fd8dd937e8 --- /dev/null +++ b/packages/code-interpreter-template/template.py @@ -0,0 +1,146 @@ +from e2b import ReadyCmd, Template, wait_for_url + + +def make_template( + kernels: list[str] = ["python", "r", "javascript", "bash", "java"], + is_docker: bool = False, + ready: ReadyCmd | None = None, + debug: bool = False, +): + enabled_kernels = set(["python", "javascript"] + kernels) + # Start with base template + template = ( + Template() + .from_image("python:3.13") + .set_user("root") + .set_workdir("/root") + .set_envs( + { + "PIP_DEFAULT_TIMEOUT": "100", + "PIP_DISABLE_PIP_VERSION_CHECK": "1", + "PIP_NO_CACHE_DIR": "1", + "JAVA_VERSION": "11", + "JAVA_HOME": "/usr/lib/jvm/jdk-${JAVA_VERSION}", + "IJAVA_VERSION": "1.3.0", + "R_VERSION": "4.5.*", + } + ) + .apt_install( + [ + "build-essential", + "curl", + "git", + "util-linux", + "jq", + "sudo", + "fonts-noto-cjk", + "ca-certificates", + ] + ) + .run_cmd("curl -fsSL https://deb.nodesource.com/setup_20.x | bash -") + .apt_install("nodejs") + .copy("requirements.txt", "requirements.txt") + .pip_install("--no-cache-dir -r requirements.txt") + ) + + if "python" in enabled_kernels: + template = template.run_cmd("ipython kernel install --name 'python3' --user") + + # Install R Kernel if requested + if "r" in enabled_kernels: + template = template.apt_install("r-base=${R_VERSION} r-base-dev").run_cmd( + [ + "R -e \"install.packages('IRkernel', repos='https://cloud.r-project.org')\"", + "R -e \"IRkernel::installspec(user = FALSE, name = 'r', displayname = 'R')\"", + ] + ) + + # Install JavaScript Kernel if requested + if "javascript" in enabled_kernels: + template = template.npm_install( + "--unsafe-perm git+https://github.com/e2b-dev/ijavascript.git", + g=True, + ).run_cmd("ijsinstall --install=global") + + # Install Bash Kernel if requested + if "bash" in enabled_kernels: + template = template.pip_install("bash_kernel").run_cmd( + "python -m bash_kernel.install" + ) + + # Install Java and Java Kernel if requested + if "java" in enabled_kernels: + template = template.run_cmd( + [ + "mkdir -p /usr/lib/jvm", + "curl -fsSL https://download.java.net/java/ga/jdk${JAVA_VERSION}/openjdk-${JAVA_VERSION}_linux-x64_bin.tar.gz | tar -xz -C /usr/lib/jvm", + "update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk-${JAVA_VERSION}/bin/java 1", + "update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/jdk-${JAVA_VERSION}/bin/javac 1", + "wget https://github.com/SpencerPark/IJava/releases/download/v${IJAVA_VERSION}/ijava-${IJAVA_VERSION}.zip", + "unzip ijava-${IJAVA_VERSION}.zip", + "python install.py --sys-prefix", + ] + ) + + # Common setup steps (always run) + template = ( + template + # Create server virtual environment + .copy("server", ".server") + .run_cmd("python -m venv .server/.venv") + # Copy and install server requirements + .run_cmd( + ".server/.venv/bin/pip install --no-cache-dir -r .server/requirements.txt" + ) + ) + + # Copy configuration files + template = ( + template.copy("matplotlibrc", ".config/matplotlib/.matplotlibrc") + .copy("jupyter-healthcheck.sh", ".jupyter/jupyter-healthcheck.sh") + .run_cmd("chmod +x .jupyter/jupyter-healthcheck.sh") + .copy("jupyter_server_config.py", ".jupyter/") + .make_dir(".ipython/profile_default/startup") + .copy("ipython_kernel_config.py", ".ipython/profile_default/") + .copy("startup_scripts", ".ipython/profile_default/startup") + ) + + if not is_docker: + template = template.copy( + "systemd/jupyter.service", "/etc/systemd/system/jupyter.service" + ).copy( + "systemd/code-interpreter.service", + "/etc/systemd/system/code-interpreter.service", + ) + if debug: + # Drop-in that routes Jupyter's stdout to the journal for debugging. + template = template.copy( + "systemd/jupyter-debug.conf", + "/etc/systemd/system/jupyter.service.d/debug.conf", + ) + else: + template = template.copy("start-up.sh", ".jupyter/start-up.sh").run_cmd( + "chmod +x .jupyter/start-up.sh" + ) + + if is_docker: + # create user user and /home/user + template = template.run_cmd("useradd -m user") + template = template.run_cmd("mkdir -p /home/user") + template = template.run_cmd("chown -R user:user /home/user") + # add to sudoers + template = template.run_cmd( + "echo 'user ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers" + ) + + template = template.set_user("user").set_workdir("/home/user") + + if is_docker: + start_cmd = "sudo --preserve-env=E2B_LOCAL /root/.jupyter/start-up.sh" + else: + start_cmd = "sudo systemctl start jupyter" + + if ready is None: + ready = wait_for_url("http://localhost:49999/health") + + return template.set_start_cmd(start_cmd, ready) diff --git a/packages/code-interpreter-template/tests/.env.example b/packages/code-interpreter-template/tests/.env.example new file mode 100644 index 0000000000..c8d7360f40 --- /dev/null +++ b/packages/code-interpreter-template/tests/.env.example @@ -0,0 +1,7 @@ +E2B_API_KEY= +# Optional, defaults to e2b.app +E2B_DOMAIN= +# Template alias/ID to create sandboxes from, defaults to code-interpreter-v1 +E2B_TESTS_TEMPLATE= +# Set to true to run against a local server on http://localhost:49999 (`make start-template-server`) +E2B_DEBUG= diff --git a/packages/code-interpreter-template/tests/README.md b/packages/code-interpreter-template/tests/README.md new file mode 100644 index 0000000000..3632293e38 --- /dev/null +++ b/packages/code-interpreter-template/tests/README.md @@ -0,0 +1,22 @@ +# Code Interpreter HTTP tests + +Run the commands below from `packages/code-interpreter-template/tests`. + +End-to-end tests for the Code Interpreter template. Sandboxes are created with +the `e2b` SDK from `E2B_TESTS_TEMPLATE` (defaults to `code-interpreter-v1`); +everything else talks to the server's HTTP API on port `49999` directly with +`httpx`, so the server protocol is what's under test, not the SDK. See +`.env.example` for configuration. + +```bash +uv sync --locked +E2B_API_KEY=... uv run pytest +``` + +Set `E2B_DEBUG=true` to run against a local server started with +`make -C .. start-template-server`; tests marked `skip_debug` (sandbox provisioning, +optional kernels, recovery) are skipped in that mode. + +A few checks need a shell inside the sandbox (systemd restarts, the private +Jupyter Unix socket in `test_jupyter_socket.py`); those use the SDK's +`sandbox.commands.run` and are otherwise asserted the same way. diff --git a/packages/code-interpreter-template/tests/charts/__init__.py b/packages/code-interpreter-template/tests/charts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/code-interpreter-template/tests/charts/conftest.py b/packages/code-interpreter-template/tests/charts/conftest.py new file mode 100644 index 0000000000..0ed451664d --- /dev/null +++ b/packages/code-interpreter-template/tests/charts/conftest.py @@ -0,0 +1,15 @@ +import pytest + +from harness import CodeInterpreter + + +@pytest.fixture() +def run_chart(client: CodeInterpreter): + def run(code: str) -> dict: + execution = client.run_code(code) + assert execution.error is None, execution.error + chart = execution.results[0].chart + assert chart + return chart + + return run diff --git a/packages/code-interpreter-template/tests/charts/test_bar.py b/packages/code-interpreter-template/tests/charts/test_bar.py new file mode 100644 index 0000000000..5f126a794a --- /dev/null +++ b/packages/code-interpreter-template/tests/charts/test_bar.py @@ -0,0 +1,43 @@ +code = """ +import matplotlib.pyplot as plt + +# Prepare data +authors = ['Author A', 'Author B', 'Author C', 'Author D'] +sales = [100, 200, 300, 400] + +# Create and customize the bar chart +plt.figure(figsize=(10, 6)) +plt.bar(authors, sales, label='Books Sold', color='blue') +plt.xlabel('Authors') +plt.ylabel('Number of Books Sold') +plt.title('Book Sales by Authors') + +# Display the chart +plt.tight_layout() +plt.show() +""" + + +def test_chart_bar(run_chart): + chart = run_chart(code) + + assert chart["type"] == "bar" + assert chart["title"] == "Book Sales by Authors" + + assert chart["x_label"] == "Authors" + assert chart["y_label"] == "Number of Books Sold" + + assert chart["x_unit"] is None + assert chart["y_unit"] is None + + bars = chart["elements"] + assert len(bars) == 4 + + assert [bar["value"] for bar in bars] == [100, 200, 300, 400] + assert [bar["label"] for bar in bars] == [ + "Author A", + "Author B", + "Author C", + "Author D", + ] + assert [bar["group"] for bar in bars] == ["Books Sold"] * 4 diff --git a/packages/code-interpreter-template/tests/charts/test_box_and_whiskers.py b/packages/code-interpreter-template/tests/charts/test_box_and_whiskers.py new file mode 100644 index 0000000000..fd978afeee --- /dev/null +++ b/packages/code-interpreter-template/tests/charts/test_box_and_whiskers.py @@ -0,0 +1,53 @@ +code = """ +import matplotlib.pyplot as plt +import numpy as np + +# Sample data +data = { + 'Class A': [85, 90, 78, 92, 88], + 'Class B': [95, 89, 76, 91, 84, 87], + 'Class C': [75, 82, 88, 79, 86] +} + +# Create figure and axis +fig, ax = plt.subplots(figsize=(10, 6)) + +# Customize plot +ax.set_title('Exam Scores Distribution') +ax.set_xlabel('Class') +ax.set_ylabel('Score') + +# Set custom colors +ax.boxplot(data.values(), labels=data.keys(), patch_artist=True) + +# Add legend +ax.legend() + +# Adjust layout and show plot +plt.tight_layout() +plt.show() +""" + + +def test_box_and_whiskers(run_chart): + chart = run_chart(code) + + assert chart["type"] == "box_and_whisker" + assert chart["title"] == "Exam Scores Distribution" + + assert chart["x_label"] == "Class" + assert chart["y_label"] == "Score" + + assert chart["x_unit"] is None + assert chart["y_unit"] is None + + bars = chart["elements"] + assert len(bars) == 3 + + assert [bar["outliers"] for bar in bars] == [[], [76], []] + assert [bar["min"] for bar in bars] == [78, 84, 75] + assert [bar["first_quartile"] for bar in bars] == [85, 84.75, 79] + assert [bar["median"] for bar in bars] == [88, 88, 82] + assert [bar["third_quartile"] for bar in bars] == [90, 90.5, 86] + assert [bar["max"] for bar in bars] == [92, 95, 88] + assert [bar["label"] for bar in bars] == ["Class A", "Class B", "Class C"] diff --git a/packages/code-interpreter-template/tests/charts/test_line.py b/packages/code-interpreter-template/tests/charts/test_line.py new file mode 100644 index 0000000000..146f81fa9e --- /dev/null +++ b/packages/code-interpreter-template/tests/charts/test_line.py @@ -0,0 +1,70 @@ +import datetime + +code = """ +import numpy as np +import matplotlib.pyplot as plt +import datetime + +# Generate x values +dates = [datetime.date(2023, 9, 1) + datetime.timedelta(seconds=i) for i in range(100)] + +x = np.linspace(0, 2*np.pi, 100) +# Calculate y values +y_sin = np.sin(x) +y_cos = np.cos(x) + +# Create the plot +plt.figure(figsize=(10, 6)) +plt.plot(dates, y_sin, label='sin(x)') +plt.plot(dates, y_cos, label='cos(x)') + +# Add labels and title +plt.xlabel("Time (s)") +plt.ylabel("Amplitude (Hz)") +plt.title('Plot of sin(x) and cos(x)') + +# Display the plot +plt.show() +""" + + +def test_line_chart(run_chart): + chart = run_chart(code) + + assert chart["type"] == "line" + assert chart["title"] == "Plot of sin(x) and cos(x)" + + assert chart["x_label"] == "Time (s)" + assert chart["y_label"] == "Amplitude (Hz)" + + assert chart["x_unit"] == "s" + assert chart["y_unit"] == "Hz" + + assert chart["x_scale"] == "datetime" + assert chart["y_scale"] == "linear" + + assert all(isinstance(x, str) for x in chart["x_ticks"]) + assert isinstance( + datetime.datetime.fromisoformat(chart["x_ticks"][0]), datetime.datetime + ) + assert all(isinstance(y, float) for y in chart["y_ticks"]) + + assert all(isinstance(x, str) for x in chart["x_tick_labels"]) + assert all(isinstance(y, str) for y in chart["y_tick_labels"]) + + lines = chart["elements"] + assert len(lines) == 2 + + first_line = lines[0] + assert first_line["label"] == "sin(x)" + assert len(first_line["points"]) == 100 + assert all( + isinstance(x, str) and isinstance(y, float) for x, y in first_line["points"] + ) + assert isinstance( + datetime.datetime.fromisoformat(first_line["points"][0][0]), datetime.datetime + ) + + second_line = lines[1] + assert second_line["label"] == "cos(x)" + assert len(second_line["points"]) == 100 diff --git a/packages/code-interpreter-template/tests/charts/test_log_chart.py b/packages/code-interpreter-template/tests/charts/test_log_chart.py new file mode 100644 index 0000000000..3c84a95a0f --- /dev/null +++ b/packages/code-interpreter-template/tests/charts/test_log_chart.py @@ -0,0 +1,55 @@ +code = """ +import numpy as np +import matplotlib.pyplot as plt + +# Generate x values +x = np.linspace(0, 100, 100) +# Calculate y values +y = np.exp(x) + +# Create the plot +plt.figure(figsize=(10, 6)) +plt.plot(x, y, label='y = e^x') + +# Set log scale for the y-axis +plt.yscale('log') + +# Add labels and title +plt.xlabel('X-axis') +plt.ylabel('Y-axis (log scale)') +plt.title('Chart with Log Scale on Y-axis') + +plt.legend() +plt.grid(True) +plt.show() +""" + + +def test_log_chart(run_chart): + chart = run_chart(code) + + assert chart["type"] == "line" + assert chart["title"] == "Chart with Log Scale on Y-axis" + + assert chart["x_label"] == "X-axis" + assert chart["y_label"] == "Y-axis (log scale)" + + assert chart["x_unit"] is None + assert chart["y_unit"] == "log scale" + + assert chart["x_scale"] == "linear" + assert chart["y_scale"] == "log" + + assert all(isinstance(x, float) for x in chart["x_ticks"]) + assert all(isinstance(y, float) for y in chart["y_ticks"]) + + assert all(isinstance(x, str) for x in chart["x_tick_labels"]) + assert all(isinstance(y, str) for y in chart["y_tick_labels"]) + + lines = chart["elements"] + assert len(lines) == 1 + + line = lines[0] + assert line["label"] == "y = e^x" + assert len(line["points"]) == 100 + assert all(isinstance(x, float) and isinstance(y, float) for x, y in line["points"]) diff --git a/packages/code-interpreter-template/tests/charts/test_pie.py b/packages/code-interpreter-template/tests/charts/test_pie.py new file mode 100644 index 0000000000..045ee6f9d8 --- /dev/null +++ b/packages/code-interpreter-template/tests/charts/test_pie.py @@ -0,0 +1,43 @@ +code = """ +import matplotlib.pyplot as plt +import numpy as np + +# Step 1: Define the data for the pie chart +categories = ["No", "No, in blue"] +sizes = [90, 10] + +# Step 2: Create the figure and axis objects +fig, ax = plt.subplots(figsize=(8, 8)) + +plt.xlabel("x") +plt.ylabel("y") + +# Step 3: Create the pie chart +ax.pie(sizes, labels=categories, autopct='%1.1f%%', startangle=90, colors=plt.cm.Pastel1.colors[:len(categories)]) + +# Step 4: Add title and legend +ax.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle +plt.title('Will I wake up early tomorrow?') + +# Step 5: Show the plot +plt.show() +""" + + +def test_pie_chart(run_chart): + chart = run_chart(code) + + assert chart["type"] == "pie" + assert chart["title"] == "Will I wake up early tomorrow?" + + assert len(chart["elements"]) == 2 + + first_data = chart["elements"][0] + assert first_data["label"] == "No" + assert first_data["angle"] == 324 + assert first_data["radius"] == 1 + + second_data = chart["elements"][1] + assert second_data["label"] == "No, in blue" + assert second_data["angle"] == 36 + assert second_data["radius"] == 1 diff --git a/packages/code-interpreter-template/tests/charts/test_scale.py b/packages/code-interpreter-template/tests/charts/test_scale.py new file mode 100644 index 0000000000..00da0eda06 --- /dev/null +++ b/packages/code-interpreter-template/tests/charts/test_scale.py @@ -0,0 +1,42 @@ +def test_datetime_scale(run_chart): + chart = run_chart( + """ + import numpy as np + import matplotlib.pyplot as plt + import datetime + + # Generate x values + dates = [datetime.date(2023, 9, 1) + datetime.timedelta(seconds=i) for i in range(100)] + y_sin = np.sin(np.linspace(0, 2*np.pi, 100)) + + # Create the plot + plt.figure(figsize=(10, 6)) + plt.plot(dates, y_sin, label='sin(x)') + plt.show() + """ + ) + + assert chart["type"] == "line" + assert chart["x_scale"] == "datetime" + assert chart["y_scale"] == "linear" + + +def test_categorical_scale(run_chart): + chart = run_chart( + """ + import numpy as np + import matplotlib.pyplot as plt + + x = [1, 2, 3, 4, 5] + y = ['A', 'B', 'C', 'D', 'E'] + + # Create the plot + plt.figure(figsize=(10, 6)) + plt.plot(x, y) + plt.show() + """ + ) + + assert chart["type"] == "line" + assert chart["x_scale"] == "linear" + assert chart["y_scale"] == "categorical" diff --git a/packages/code-interpreter-template/tests/charts/test_scatter.py b/packages/code-interpreter-template/tests/charts/test_scatter.py new file mode 100644 index 0000000000..02c8672ed4 --- /dev/null +++ b/packages/code-interpreter-template/tests/charts/test_scatter.py @@ -0,0 +1,50 @@ +code = """ +import matplotlib.pyplot as plt +import numpy as np + +# Create data +N = 5 +x1 = np.random.rand(N) +y1 = np.random.rand(N) +x2 = np.random.rand(2*N) +y2 = np.random.rand(2*N) + +plt.xlabel("A") +plt.ylabel("B") + +plt.scatter(x1, y1, c='blue', label='Dataset 1') +plt.scatter(x2, y2, c='red', label='Dataset 2') + +plt.show() +""" + + +def test_scatter_chart(run_chart): + chart = run_chart(code) + + assert chart["type"] == "scatter" + + assert chart["title"] is None + assert chart["x_label"] == "A" + assert chart["y_label"] == "B" + + assert chart["x_scale"] == "linear" + assert chart["y_scale"] == "linear" + + assert all(isinstance(x, float) for x in chart["x_ticks"]) + assert all(isinstance(y, float) for y in chart["y_ticks"]) + + assert all(isinstance(x, str) for x in chart["x_tick_labels"]) + assert all(isinstance(y, str) for y in chart["y_tick_labels"]) + + assert len(chart["elements"]) == 2 + + first_data = chart["elements"][0] + assert first_data["label"] == "Dataset 1" + assert len(first_data["points"]) == 5 + assert all(len(point) == 2 for point in first_data["points"]) + + second_data = chart["elements"][1] + assert second_data["label"] == "Dataset 2" + assert len(second_data["points"]) == 10 + assert all(len(point) == 2 for point in second_data["points"]) diff --git a/packages/code-interpreter-template/tests/charts/test_superchart.py b/packages/code-interpreter-template/tests/charts/test_superchart.py new file mode 100644 index 0000000000..37a64b9aa0 --- /dev/null +++ b/packages/code-interpreter-template/tests/charts/test_superchart.py @@ -0,0 +1,55 @@ +code = """ +import matplotlib.pyplot as plt +import numpy as np + +# Data for plotting +x1 = np.linspace(0, 10, 100) +y1 = np.sin(x1) + +# Create a figure with multiple subplots +fig, axs = plt.subplots(1, 2, figsize=(10, 8)) +fig.suptitle('Multiple Charts Example', fontsize=16) + +# Plotting on the different axes +axs[0].plot(x1, y1, 'r') +axs[0].set_title('Sine Wave') +axs[0].grid(True) + +N = 5 +x2 = np.random.rand(N) +y2 = np.random.rand(N) + +axs[1].scatter(x2, y2, c='blue', label='Dataset 1') +axs[1].set_xlabel('X') +axs[1].set_ylabel('Y') +axs[1].set_title('Scatter Plot') +axs[1].grid(True) + +plt.show() +""" + + +def test_super_chart(run_chart): + chart = run_chart(code) + + assert chart["type"] == "superchart" + assert chart["title"] == "Multiple Charts Example" + + charts = chart["elements"] + assert len(charts) == 2 + + first_chart = charts[0] + assert first_chart["type"] == "line" + assert first_chart["title"] == "Sine Wave" + assert first_chart["x_label"] is None + assert first_chart["y_label"] is None + assert len(first_chart["elements"]) == 1 + assert len(first_chart["elements"][0]["points"]) == 100 + + second_chart = charts[1] + assert second_chart["type"] == "scatter" + assert second_chart["title"] == "Scatter Plot" + assert second_chart["x_label"] == "X" + assert second_chart["y_label"] == "Y" + assert len(second_chart["elements"]) == 1 + assert len(second_chart["elements"][0]["points"]) == 5 diff --git a/packages/code-interpreter-template/tests/charts/test_unknown.py b/packages/code-interpreter-template/tests/charts/test_unknown.py new file mode 100644 index 0000000000..fba4273c30 --- /dev/null +++ b/packages/code-interpreter-template/tests/charts/test_unknown.py @@ -0,0 +1,33 @@ +code = """ +import matplotlib.pyplot as plt +import numpy as np + +# Create a figure and an axis +fig, ax = plt.subplots() + +# Create data for two concentric circles +circle1 = plt.Circle((0, 0), 1, color='blue', fill=False, linewidth=2) +circle2 = plt.Circle((0, 0), 2, color='red', fill=False, linewidth=2) + +# Add the circles to the axes +ax.add_artist(circle1) +ax.add_artist(circle2) + +# Set grid +ax.grid(True) + +# Set title +plt.title('Two Concentric Circles') + +# Show the plot +plt.show() +""" + + +def test_unknown_charts(run_chart): + chart = run_chart(code) + + assert chart["type"] == "unknown" + assert chart["title"] == "Two Concentric Circles" + + assert chart["elements"] == [] diff --git a/packages/code-interpreter-template/tests/conftest.py b/packages/code-interpreter-template/tests/conftest.py new file mode 100644 index 0000000000..fc06d9d36d --- /dev/null +++ b/packages/code-interpreter-template/tests/conftest.py @@ -0,0 +1,117 @@ +import os +import uuid +from typing import Callable, Optional + +import pytest +from dotenv import load_dotenv +from e2b import Sandbox + +from harness import AsyncCodeInterpreter, CodeInterpreter + +load_dotenv() + +DEFAULT_TEST_SANDBOX_TIMEOUT = 120 +CODE_INTERPRETER_PORT = 49999 +LOCAL_SERVER_URL = f"http://localhost:{CODE_INTERPRETER_PORT}" + + +def is_debug() -> bool: + return os.getenv("E2B_DEBUG", "false").lower() == "true" + + +@pytest.fixture(scope="session") +def sandbox_test_id() -> str: + return f"test_{uuid.uuid4()}" + + +@pytest.fixture(scope="session") +def template() -> str: + return os.getenv("E2B_TESTS_TEMPLATE") or "code-interpreter-v1" + + +@pytest.fixture() +def sandbox_factory( + request: pytest.FixtureRequest, template: str, sandbox_test_id: str +) -> Callable[..., Sandbox]: + # Skip lazily, on use: the debug branches of `client`/`async_client` + # depend on this fixture without ever calling it. + def factory( + *, + timeout: int = DEFAULT_TEST_SANDBOX_TIMEOUT, + envs: Optional[dict[str, str]] = None, + allow_public_traffic: bool = True, + ) -> Sandbox: + if is_debug(): + pytest.skip("Sandbox provisioning is not available in debug mode") + sandbox = Sandbox.create( + template, + timeout=timeout, + metadata={"sandbox_test_id": sandbox_test_id}, + envs=envs, + network={"allow_public_traffic": allow_public_traffic}, + ) + request.addfinalizer(sandbox.kill) + return sandbox + + return factory + + +@pytest.fixture() +def sandbox(sandbox_factory) -> Sandbox: + return sandbox_factory() + + +def code_interpreter_url(sandbox: Sandbox) -> str: + scheme = "http" if sandbox.connection_config.debug else "https" + return f"{scheme}://{sandbox.get_host(CODE_INTERPRETER_PORT)}" + + +def make_client(sandbox: Sandbox) -> CodeInterpreter: + return CodeInterpreter( + code_interpreter_url(sandbox), + envd_access_token=sandbox._envd_access_token, + traffic_access_token=sandbox.traffic_access_token, + ) + + +def make_async_client(sandbox: Sandbox) -> AsyncCodeInterpreter: + return AsyncCodeInterpreter( + code_interpreter_url(sandbox), + envd_access_token=sandbox._envd_access_token, + traffic_access_token=sandbox.traffic_access_token, + ) + + +@pytest.fixture() +def client_factory(request: pytest.FixtureRequest, sandbox_factory): + def factory(**sandbox_kwargs) -> CodeInterpreter: + client = make_client(sandbox_factory(**sandbox_kwargs)) + request.addfinalizer(client.close) + return client + + return factory + + +@pytest.fixture() +def client(request: pytest.FixtureRequest, client_factory) -> CodeInterpreter: + if is_debug(): + client = CodeInterpreter(LOCAL_SERVER_URL) + request.addfinalizer(client.close) + return client + return client_factory() + + +@pytest.fixture() +async def async_client(request: pytest.FixtureRequest, sandbox_factory): + if is_debug(): + client = AsyncCodeInterpreter(LOCAL_SERVER_URL) + else: + client = make_async_client(sandbox_factory()) + yield client + await client.aclose() + + +@pytest.fixture(autouse=True) +def skip_debug(request: pytest.FixtureRequest): + if request.node.get_closest_marker("skip_debug") and is_debug(): + pytest.skip("Skipped in debug mode") diff --git a/packages/code-interpreter-template/tests/env_vars/__init__.py b/packages/code-interpreter-template/tests/env_vars/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/code-interpreter-template/tests/env_vars/test_bash.py b/packages/code-interpreter-template/tests/env_vars/test_bash.py new file mode 100644 index 0000000000..3d3b639e2b --- /dev/null +++ b/packages/code-interpreter-template/tests/env_vars/test_bash.py @@ -0,0 +1,30 @@ +import pytest + +from harness import CodeInterpreter + + +@pytest.mark.skip_debug +def test_env_vars_on_sandbox(client_factory): + client = client_factory(envs={"TEST_ENV_VAR": "supertest"}) + execution = client.run_code("echo $TEST_ENV_VAR", language="bash") + assert execution.stdout[0] == "supertest\n" + + +def test_env_vars_per_execution(client: CodeInterpreter): + execution = client.run_code("echo $FOO", envs={"FOO": "bar"}, language="bash") + execution_empty = client.run_code("echo ${FOO:-default}", language="bash") + + assert execution.stdout[0] == "bar\n" + assert execution_empty.stdout[0] == "default\n" + + +@pytest.mark.skip_debug +def test_env_vars_overwrite(client_factory): + client = client_factory(envs={"TEST_ENV_VAR": "supertest"}) + execution = client.run_code( + "echo $TEST_ENV_VAR", language="bash", envs={"TEST_ENV_VAR": "overwrite"} + ) + execution_global_default = client.run_code("echo $TEST_ENV_VAR", language="bash") + + assert execution.stdout[0] == "overwrite\n" + assert execution_global_default.stdout[0] == "supertest\n" diff --git a/packages/code-interpreter-template/tests/env_vars/test_java.py b/packages/code-interpreter-template/tests/env_vars/test_java.py new file mode 100644 index 0000000000..e719f29846 --- /dev/null +++ b/packages/code-interpreter-template/tests/env_vars/test_java.py @@ -0,0 +1,46 @@ +import pytest + +from harness import CodeInterpreter + +pytestmark = pytest.mark.skip_debug + + +def test_env_vars_on_sandbox(client_factory): + client = client_factory(envs={"TEST_ENV_VAR": "supertest"}) + client.wait_for_kernel("java") + execution = client.run_code('System.getProperty("TEST_ENV_VAR")', language="java") + assert execution.text is not None + assert execution.text.strip() == "supertest" + + +def test_env_vars_per_execution(client: CodeInterpreter): + client.wait_for_kernel("java") + execution = client.run_code( + 'System.getProperty("FOO")', envs={"FOO": "bar"}, language="java" + ) + execution_empty = client.run_code( + 'System.getProperty("FOO", "default")', language="java" + ) + + assert execution.text is not None + assert execution.text.strip() == "bar" + assert execution_empty.text is not None + assert execution_empty.text.strip() == "default" + + +def test_env_vars_overwrite(client_factory): + client = client_factory(envs={"TEST_ENV_VAR": "supertest"}) + client.wait_for_kernel("java") + execution = client.run_code( + 'System.getProperty("TEST_ENV_VAR")', + language="java", + envs={"TEST_ENV_VAR": "overwrite"}, + ) + execution_global_default = client.run_code( + 'System.getProperty("TEST_ENV_VAR")', language="java" + ) + + assert execution.text is not None + assert execution.text.strip() == "overwrite" + assert execution_global_default.text is not None + assert execution_global_default.text.strip() == "supertest" diff --git a/packages/code-interpreter-template/tests/env_vars/test_js.py b/packages/code-interpreter-template/tests/env_vars/test_js.py new file mode 100644 index 0000000000..93646655dd --- /dev/null +++ b/packages/code-interpreter-template/tests/env_vars/test_js.py @@ -0,0 +1,43 @@ +import pytest + +from harness import CodeInterpreter + + +@pytest.mark.skip_debug +def test_env_vars_on_sandbox(client_factory): + client = client_factory(envs={"TEST_ENV_VAR": "supertest"}) + execution = client.run_code("process.env.TEST_ENV_VAR", language="javascript") + assert execution.text is not None + assert execution.text.strip() == "supertest" + + +def test_env_vars_per_execution(client: CodeInterpreter): + execution = client.run_code( + "process.env.FOO", envs={"FOO": "bar"}, language="javascript" + ) + execution_empty = client.run_code( + "process.env.FOO || 'default'", language="javascript" + ) + + assert execution.text is not None + assert execution.text.strip() == "bar" + assert execution_empty.text is not None + assert execution_empty.text.strip() == "default" + + +@pytest.mark.skip_debug +def test_env_vars_overwrite(client_factory): + client = client_factory(envs={"TEST_ENV_VAR": "supertest"}) + execution = client.run_code( + "process.env.TEST_ENV_VAR", + language="javascript", + envs={"TEST_ENV_VAR": "overwrite"}, + ) + execution_global_default = client.run_code( + "process.env.TEST_ENV_VAR", language="javascript" + ) + + assert execution.text is not None + assert execution.text.strip() == "overwrite" + assert execution_global_default.text is not None + assert execution_global_default.text.strip() == "supertest" diff --git a/packages/code-interpreter-template/tests/env_vars/test_python.py b/packages/code-interpreter-template/tests/env_vars/test_python.py new file mode 100644 index 0000000000..729528d53b --- /dev/null +++ b/packages/code-interpreter-template/tests/env_vars/test_python.py @@ -0,0 +1,40 @@ +import pytest + +from harness import CodeInterpreter + + +@pytest.mark.skip_debug +def test_env_vars_on_sandbox(client_factory): + client = client_factory(envs={"TEST_ENV_VAR": "supertest"}) + execution = client.run_code( + "import os; os.getenv('TEST_ENV_VAR')", language="python" + ) + assert execution.text == "supertest" + + +def test_env_vars_per_execution(client: CodeInterpreter): + execution = client.run_code( + "import os; os.getenv('FOO')", envs={"FOO": "bar"}, language="python" + ) + execution_empty = client.run_code( + "import os; os.getenv('FOO', 'default')", language="python" + ) + + assert execution.text == "bar" + assert execution_empty.text == "default" + + +@pytest.mark.skip_debug +def test_env_vars_overwrite(client_factory): + client = client_factory(envs={"TEST_ENV_VAR": "supertest"}) + execution = client.run_code( + "import os; os.getenv('TEST_ENV_VAR')", + language="python", + envs={"TEST_ENV_VAR": "overwrite"}, + ) + execution_global_default = client.run_code( + "import os; os.getenv('TEST_ENV_VAR')", language="python" + ) + + assert execution.text == "overwrite" + assert execution_global_default.text == "supertest" diff --git a/packages/code-interpreter-template/tests/env_vars/test_r.py b/packages/code-interpreter-template/tests/env_vars/test_r.py new file mode 100644 index 0000000000..c16568dced --- /dev/null +++ b/packages/code-interpreter-template/tests/env_vars/test_r.py @@ -0,0 +1,38 @@ +import pytest + +from harness import CodeInterpreter + + +@pytest.mark.skip_debug +def test_env_vars_on_sandbox(client_factory): + client = client_factory(envs={"TEST_ENV_VAR": "supertest"}) + client.wait_for_kernel("r") + execution = client.run_code("Sys.getenv('TEST_ENV_VAR')", language="r") + assert execution.results[0].text.strip() == '[1] "supertest"' + + +@pytest.mark.skip_debug +def test_env_vars_per_execution(client: CodeInterpreter): + client.wait_for_kernel("r") + execution = client.run_code("Sys.getenv('FOO')", envs={"FOO": "bar"}, language="r") + execution_empty = client.run_code( + "Sys.getenv('FOO', unset = 'default')", language="r" + ) + + assert execution.results[0].text.strip() == '[1] "bar"' + assert execution_empty.results[0].text.strip() == '[1] "default"' + + +@pytest.mark.skip_debug +def test_env_vars_overwrite(client_factory): + client = client_factory(envs={"TEST_ENV_VAR": "supertest"}) + client.wait_for_kernel("r") + execution = client.run_code( + "Sys.getenv('TEST_ENV_VAR')", language="r", envs={"TEST_ENV_VAR": "overwrite"} + ) + execution_global_default = client.run_code( + "Sys.getenv('TEST_ENV_VAR')", language="r" + ) + + assert execution.results[0].text.strip() == '[1] "overwrite"' + assert execution_global_default.results[0].text.strip() == '[1] "supertest"' diff --git a/packages/code-interpreter-template/tests/harness/__init__.py b/packages/code-interpreter-template/tests/harness/__init__.py new file mode 100644 index 0000000000..b1e89772c5 --- /dev/null +++ b/packages/code-interpreter-template/tests/harness/__init__.py @@ -0,0 +1,17 @@ +from .code_interpreter import ( + AsyncCodeInterpreter, + CodeInterpreter, + CodeInterpreterError, + Execution, + ExecutionError, + Result, +) + +__all__ = [ + "AsyncCodeInterpreter", + "CodeInterpreter", + "CodeInterpreterError", + "Execution", + "ExecutionError", + "Result", +] diff --git a/packages/code-interpreter-template/tests/harness/code_interpreter.py b/packages/code-interpreter-template/tests/harness/code_interpreter.py new file mode 100644 index 0000000000..88a2912fd2 --- /dev/null +++ b/packages/code-interpreter-template/tests/harness/code_interpreter.py @@ -0,0 +1,303 @@ +"""httpx client for the Code Interpreter HTTP API (port 49999). + +Mirrors the wire protocol only: JSON requests and a newline-delimited JSON +stream of output events terminated by ``{"type": "end_of_execution"}``. +""" + +import json +import textwrap +from dataclasses import dataclass, field +from typing import AsyncIterator, Iterator, Optional + +import httpx + +KERNEL_READINESS_ATTEMPTS = 4 +DEFAULT_EXECUTION_TIMEOUT = 60 +DEFAULT_REQUEST_TIMEOUT = 30 + +DATA_FORMATS = ( + "text", + "html", + "markdown", + "svg", + "png", + "jpeg", + "pdf", + "latex", + "json", + "javascript", + "data", + "chart", +) + + +class CodeInterpreterError(Exception): + def __init__(self, response: httpx.Response): + self.status_code = response.status_code + self.body = response.text + super().__init__( + f"{response.status_code} {response.reason_phrase}: {self.body}" + ) + + +@dataclass +class Result: + raw: dict + + def __getattr__(self, name: str): + if name in DATA_FORMATS or name in ("extra", "is_main_result"): + return self.raw.get(name) + raise AttributeError(name) + + def formats(self) -> list[str]: + return [fmt for fmt in DATA_FORMATS if self.raw.get(fmt) is not None] + + +@dataclass +class ExecutionError: + name: str + value: str + traceback: str + + +@dataclass +class Execution: + events: list[dict] = field(default_factory=list) + results: list[Result] = field(default_factory=list) + stdout: list[str] = field(default_factory=list) + stderr: list[str] = field(default_factory=list) + error: Optional[ExecutionError] = None + execution_count: Optional[int] = None + completed: bool = False + + @property + def text(self) -> Optional[str]: + for result in self.results: + if result.is_main_result: + return result.text + return None + + def add(self, event: dict) -> None: + self.events.append(event) + kind = event["type"] + if kind == "stdout": + self.stdout.append(event["text"]) + elif kind == "stderr": + self.stderr.append(event["text"]) + elif kind == "result": + self.results.append(Result({k: v for k, v in event.items() if k != "type"})) + elif kind == "error": + self.error = ExecutionError( + name=event["name"], value=event["value"], traceback=event["traceback"] + ) + elif kind == "number_of_executions": + self.execution_count = event["execution_count"] + elif kind == "end_of_execution": + self.completed = True + + @classmethod + def from_events(cls, events) -> "Execution": + execution = cls() + for event in events: + execution.add(event) + return execution + + +def _headers( + envd_access_token: Optional[str], traffic_access_token: Optional[str] +) -> dict[str, str]: + headers = {} + if envd_access_token: + headers["X-Access-Token"] = envd_access_token + if traffic_access_token: + headers["E2B-Traffic-Access-Token"] = traffic_access_token + return headers + + +def _execute_body( + code: str, + language: Optional[str], + context_id: Optional[str], + envs: Optional[dict[str, str]], +) -> dict: + return { + "code": textwrap.dedent(code), + "language": language, + "context_id": context_id, + "env_vars": envs, + } + + +def _is_readiness_error(error: Exception) -> bool: + return isinstance(error, CodeInterpreterError) and error.status_code == 500 + + +class CodeInterpreter: + def __init__( + self, + base_url: str, + *, + envd_access_token: Optional[str] = None, + traffic_access_token: Optional[str] = None, + ): + self.base_url = base_url + self._client = httpx.Client( + base_url=base_url, + headers=_headers(envd_access_token, traffic_access_token), + timeout=DEFAULT_REQUEST_TIMEOUT, + ) + + def close(self) -> None: + self._client.close() + + def health(self, timeout: float = DEFAULT_REQUEST_TIMEOUT) -> httpx.Response: + return self._client.get("/health", timeout=timeout) + + def stream_code( + self, + code: str, + *, + language: Optional[str] = None, + context_id: Optional[str] = None, + envs: Optional[dict[str, str]] = None, + timeout: float = DEFAULT_EXECUTION_TIMEOUT, + ) -> Iterator[dict]: + with self._client.stream( + "POST", + "/execute", + json=_execute_body(code, language, context_id, envs), + timeout=httpx.Timeout(timeout, connect=DEFAULT_REQUEST_TIMEOUT), + ) as response: + if response.status_code >= 400: + response.read() + raise CodeInterpreterError(response) + for line in response.iter_lines(): + if line: + yield json.loads(line) + + def run_code( + self, + code: str, + *, + language: Optional[str] = None, + context_id: Optional[str] = None, + envs: Optional[dict[str, str]] = None, + timeout: float = DEFAULT_EXECUTION_TIMEOUT, + ) -> Execution: + return Execution.from_events( + self.stream_code( + code, + language=language, + context_id=context_id, + envs=envs, + timeout=timeout, + ) + ) + + def wait_for_kernel(self, language: str) -> None: + for attempt in range(KERNEL_READINESS_ATTEMPTS): + try: + self.run_code("1", language=language) + return + except CodeInterpreterError as error: + if ( + not _is_readiness_error(error) + or attempt == KERNEL_READINESS_ATTEMPTS - 1 + ): + raise + + def _check(self, response: httpx.Response) -> httpx.Response: + if response.status_code >= 400: + raise CodeInterpreterError(response) + return response + + def create_context( + self, *, language: Optional[str] = None, cwd: Optional[str] = None + ) -> dict: + body = {} + if language is not None: + body["language"] = language + if cwd is not None: + body["cwd"] = cwd + return self._check(self._client.post("/contexts", json=body)).json() + + def list_contexts(self) -> list[dict]: + return self._check(self._client.get("/contexts")).json() + + def restart_context(self, context_id: str) -> None: + self._check(self._client.post(f"/contexts/{context_id}/restart")) + + def remove_context(self, context_id: str) -> None: + self._check(self._client.delete(f"/contexts/{context_id}")) + + +class AsyncCodeInterpreter: + def __init__( + self, + base_url: str, + *, + envd_access_token: Optional[str] = None, + traffic_access_token: Optional[str] = None, + ): + self.base_url = base_url + self._client = httpx.AsyncClient( + base_url=base_url, + headers=_headers(envd_access_token, traffic_access_token), + timeout=DEFAULT_REQUEST_TIMEOUT, + ) + + async def aclose(self) -> None: + await self._client.aclose() + + async def health(self, timeout: float = DEFAULT_REQUEST_TIMEOUT) -> httpx.Response: + return await self._client.get("/health", timeout=timeout) + + async def stream_code( + self, + code: str, + *, + language: Optional[str] = None, + context_id: Optional[str] = None, + envs: Optional[dict[str, str]] = None, + timeout: float = DEFAULT_EXECUTION_TIMEOUT, + ) -> AsyncIterator[dict]: + async with self._client.stream( + "POST", + "/execute", + json=_execute_body(code, language, context_id, envs), + timeout=httpx.Timeout(timeout, connect=DEFAULT_REQUEST_TIMEOUT), + ) as response: + if response.status_code >= 400: + await response.aread() + raise CodeInterpreterError(response) + async for line in response.aiter_lines(): + if line: + yield json.loads(line) + + async def run_code( + self, + code: str, + *, + language: Optional[str] = None, + context_id: Optional[str] = None, + envs: Optional[dict[str, str]] = None, + timeout: float = DEFAULT_EXECUTION_TIMEOUT, + ) -> Execution: + execution = Execution() + async for event in self.stream_code( + code, language=language, context_id=context_id, envs=envs, timeout=timeout + ): + execution.add(event) + return execution + + async def wait_for_kernel(self, language: str) -> None: + for attempt in range(KERNEL_READINESS_ATTEMPTS): + try: + await self.run_code("1", language=language) + return + except CodeInterpreterError as error: + if ( + not _is_readiness_error(error) + or attempt == KERNEL_READINESS_ATTEMPTS - 1 + ): + raise diff --git a/packages/code-interpreter-template/tests/pyproject.toml b/packages/code-interpreter-template/tests/pyproject.toml new file mode 100644 index 0000000000..1fd6edb272 --- /dev/null +++ b/packages/code-interpreter-template/tests/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "e2b-code-interpreter-tests" +version = "0.0.0" +description = "HTTP-level test suite for the E2B Code Interpreter template" +requires-python = ">=3.10" +dependencies = [ + "e2b>=2.32.0,<3", + "httpx>=0.28.1,<1", + "pytest>=9.0.3,<10", + "pytest-asyncio>=1.3.0,<2", + "pytest-dotenv>=0.5.2,<0.6", + "pytest-xdist>=3.6.1,<4", + "python-dotenv>=1.2.1,<2", + "ruff>=0.11.12,<0.12", +] + +[tool.uv] +package = false + +[tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" +asyncio_default_test_loop_scope = "session" +addopts = ["--import-mode=importlib", "--numprocesses=2"] +pythonpath = ["."] +markers = [ + "skip_debug: skip when running against a local template server (E2B_DEBUG=true)", +] diff --git a/packages/code-interpreter-template/tests/test_async.py b/packages/code-interpreter-template/tests/test_async.py new file mode 100644 index 0000000000..975e094791 --- /dev/null +++ b/packages/code-interpreter-template/tests/test_async.py @@ -0,0 +1,46 @@ +import asyncio + +import httpx +import pytest + +from harness import AsyncCodeInterpreter + + +async def test_async_basic(async_client: AsyncCodeInterpreter): + execution = await async_client.run_code("x =1; x") + assert execution.text == "1" + + +async def test_async_streaming(async_client: AsyncCodeInterpreter): + events = [event async for event in async_client.stream_code("print(1); 2")] + + assert [event["type"] for event in events] == [ + "number_of_executions", + "stdout", + "result", + "end_of_execution", + ] + assert events[1]["text"] == "1\n" + assert events[2]["text"] == "2" + + +async def test_async_concurrent_executions(async_client: AsyncCodeInterpreter): + executions = await asyncio.gather( + async_client.run_code("import time; time.sleep(1); 'python'"), + async_client.run_code( + "await new Promise(r => setTimeout(r, 1000)); 'js'", language="js" + ), + ) + + assert executions[0].text == "python" + assert executions[1].text == "js" + + +async def test_async_interrupt(async_client: AsyncCodeInterpreter): + with pytest.raises(httpx.TimeoutException): + await async_client.run_code("import time; time.sleep(300)", timeout=3) + + await asyncio.sleep(5) + + execution = await async_client.run_code("1 + 1", timeout=10) + assert execution.text == "2" diff --git a/packages/code-interpreter-template/tests/test_basic.py b/packages/code-interpreter-template/tests/test_basic.py new file mode 100644 index 0000000000..62d3d3d69b --- /dev/null +++ b/packages/code-interpreter-template/tests/test_basic.py @@ -0,0 +1,75 @@ +import pytest +from e2b import Sandbox + +from conftest import make_client +from harness import CodeInterpreter + + +def test_health(client: CodeInterpreter): + response = client.health() + assert response.status_code == 200 + assert response.json() == "OK" + + +def test_basic(client: CodeInterpreter): + execution = client.run_code("x =1; x") + assert execution.text == "1" + assert execution.completed + + +def test_stateful(client: CodeInterpreter): + client.run_code("test_stateful = 1") + + execution = client.run_code("test_stateful+=1; test_stateful") + assert execution.text == "2" + + +def test_stdout(client: CodeInterpreter): + execution = client.run_code("print('Hello from e2b')") + assert execution.stdout == ["Hello from e2b\n"] + + +def test_stderr(client: CodeInterpreter): + execution = client.run_code( + 'import sys;print("This is an error message", file=sys.stderr)' + ) + assert execution.stderr == ["This is an error message\n"] + + +def test_error(client: CodeInterpreter): + execution = client.run_code("xyz") + assert execution.error is not None + assert execution.error.name == "NameError" + assert execution.error.value == "name 'xyz' is not defined" + assert "NameError" in execution.error.traceback + + +def test_bash_magic(client: CodeInterpreter): + execution = client.run_code("!pwd") + assert "".join(execution.stdout).strip() == "/home/user" + + +@pytest.mark.skip_debug +def test_execution_count(client: CodeInterpreter): + client.run_code("echo 'E2B is awesome!'") + execution = client.run_code("!pwd") + assert execution.execution_count == 2 + + +@pytest.mark.skip_debug +def test_secure_access(client_factory): + client = client_factory(allow_public_traffic=False) + execution = client.run_code("x =1; x") + assert execution.text == "1" + + +@pytest.mark.skip_debug +def test_reconnected_sandbox_keeps_state(sandbox: Sandbox): + first = make_client(sandbox) + first.run_code("x = 1") + first.close() + + second = make_client(Sandbox.connect(sandbox.sandbox_id)) + execution = second.run_code("x") + second.close() + assert execution.text == "1" diff --git a/packages/code-interpreter-template/tests/test_contexts.py b/packages/code-interpreter-template/tests/test_contexts.py new file mode 100644 index 0000000000..8c2c476463 --- /dev/null +++ b/packages/code-interpreter-template/tests/test_contexts.py @@ -0,0 +1,118 @@ +import pytest + +from harness import CodeInterpreter, CodeInterpreterError + + +def test_create_context_with_no_options(client: CodeInterpreter): + context = client.create_context() + + assert context["language"] == "python" + assert context["cwd"] == "/home/user" + assert client.list_contexts()[-1] == context + + +def test_create_context_with_options(client: CodeInterpreter): + context = client.create_context(language="python", cwd="/root") + + assert context["language"] == "python" + assert context["cwd"] == "/root" + assert client.list_contexts()[-1] == context + + +@pytest.mark.parametrize( + ("alias", "language"), [("js", "javascript"), ("ts", "typescript")] +) +def test_create_context_normalizes_language_alias( + client: CodeInterpreter, alias: str, language: str +): + context = client.create_context(language=alias) + assert context["language"] == language + + +def test_remove_context(client: CodeInterpreter): + context = client.create_context() + + client.remove_context(context["id"]) + + assert context["id"] not in [ctx["id"] for ctx in client.list_contexts()] + + +def test_list_contexts(client: CodeInterpreter): + languages = [context["language"] for context in client.list_contexts()] + assert "python" in languages + assert "javascript" in languages + + +def test_restart_context(client: CodeInterpreter): + context = client.create_context() + + client.run_code("x = 1", context_id=context["id"]) + client.restart_context(context["id"]) + + execution = client.run_code("x", context_id=context["id"]) + assert execution.error is not None + assert execution.error.name == "NameError" + assert execution.error.value == "name 'x' is not defined" + + +def test_independence_of_contexts(client: CodeInterpreter): + context = client.create_context() + client.run_code("x = 1") + + execution = client.run_code("x", context_id=context["id"]) + assert execution.error is not None + assert execution.error.value == "name 'x' is not defined" + + +def test_pass_context_and_language(client: CodeInterpreter): + context = client.create_context(language="python") + + with pytest.raises(CodeInterpreterError) as error: + client.run_code( + "console.log('Hello, World!')", language="js", context_id=context["id"] + ) + + assert error.value.status_code == 400 + assert "Only one of context_id or language" in error.value.body + + +def test_unknown_context(client: CodeInterpreter): + with pytest.raises(CodeInterpreterError) as error: + client.run_code("1", context_id="does-not-exist") + + assert error.value.status_code == 404 + + +def test_restart_unknown_context(client: CodeInterpreter): + with pytest.raises(CodeInterpreterError) as error: + client.restart_context("does-not-exist") + + assert error.value.status_code == 404 + + +def test_remove_unknown_context(client: CodeInterpreter): + with pytest.raises(CodeInterpreterError) as error: + client.remove_context("does-not-exist") + + assert error.value.status_code == 404 + + +@pytest.mark.skip_debug +def test_contexts_secure_traffic(client_factory): + client = client_factory(allow_public_traffic=False) + + languages = [context["language"] for context in client.list_contexts()] + assert "python" in languages + assert "javascript" in languages + + context = client.create_context() + assert client.list_contexts()[-1] == context + + client.run_code("x = 1", context_id=context["id"]) + client.restart_context(context["id"]) + execution = client.run_code("x", context_id=context["id"]) + assert execution.error is not None + assert execution.error.name == "NameError" + + client.remove_context(context["id"]) + assert context["id"] not in [ctx["id"] for ctx in client.list_contexts()] diff --git a/packages/code-interpreter-template/tests/test_cwd.py b/packages/code-interpreter-template/tests/test_cwd.py new file mode 100644 index 0000000000..ba5274a294 --- /dev/null +++ b/packages/code-interpreter-template/tests/test_cwd.py @@ -0,0 +1,43 @@ +import pytest + +from harness import CodeInterpreter + +pytestmark = pytest.mark.skip_debug + + +def test_cwd_python(client: CodeInterpreter): + execution = client.run_code("from pathlib import Path; print(Path.cwd())") + assert "".join(execution.stdout).strip() == "/home/user" + + +def test_cwd_javascript(client: CodeInterpreter): + execution = client.run_code("process.cwd()", language="js") + assert execution.text == "/home/user" + + +def test_cwd_typescript(client: CodeInterpreter): + execution = client.run_code("process.cwd()", language="ts") + assert execution.text == "/home/user" + + +def test_cwd_r(client: CodeInterpreter): + client.wait_for_kernel("r") + execution = client.run_code("getwd()", language="r") + assert execution.results[0].text.strip() == '[1] "/home/user"' + + +def test_cwd_java(client: CodeInterpreter): + client.wait_for_kernel("java") + execution = client.run_code('System.getProperty("user.dir")', language="java") + assert execution.results[0].text.strip() == "/home/user" + + +def test_cwd_bash(client: CodeInterpreter): + execution = client.run_code("pwd", language="bash") + assert "".join(execution.stdout).strip() == "/home/user" + + +def test_custom_cwd_context(client: CodeInterpreter): + context = client.create_context(language="python", cwd="/tmp") + execution = client.run_code("import os; os.getcwd()", context_id=context["id"]) + assert execution.text == "/tmp" diff --git a/packages/code-interpreter-template/tests/test_interrupt.py b/packages/code-interpreter-template/tests/test_interrupt.py new file mode 100644 index 0000000000..cda1de46f3 --- /dev/null +++ b/packages/code-interpreter-template/tests/test_interrupt.py @@ -0,0 +1,20 @@ +import time + +import httpx +import pytest + +from harness import CodeInterpreter + + +def test_subsequent_execution_works_after_client_timeout(client: CodeInterpreter): + # Start a long-running execution with a short client timeout. + # Closing the connection should trigger the server to interrupt the + # kernel (#213) instead of leaving it busy. + with pytest.raises(httpx.TimeoutException): + client.run_code("import time; time.sleep(300)", timeout=3) + + # Give the server time to detect the disconnect and interrupt the kernel. + time.sleep(5) + + execution = client.run_code("1 + 1", timeout=10) + assert execution.text == "2" diff --git a/packages/code-interpreter-template/tests/test_jupyter_socket.py b/packages/code-interpreter-template/tests/test_jupyter_socket.py new file mode 100644 index 0000000000..ae20400991 --- /dev/null +++ b/packages/code-interpreter-template/tests/test_jupyter_socket.py @@ -0,0 +1,42 @@ +import pytest +from e2b import CommandExitException, Sandbox + +from conftest import make_client + +pytestmark = pytest.mark.skip_debug + +SOCKET_DIR = "/run/e2b-jupyter" +SOCKET_PATH = f"{SOCKET_DIR}/server.sock" +CURL_STATUS = ( + f"curl -s -o /dev/null -w '%{{http_code}}' --unix-socket {SOCKET_PATH} " + "http://localhost/api/status" +) + + +def test_socket_is_private_to_root(sandbox: Sandbox): + result = sandbox.commands.run( + f"stat -c '%a %U' {SOCKET_DIR} {SOCKET_PATH}", user="root" + ) + assert result.stdout.split("\n")[:2] == ["700 root", "600 root"] + + +def test_jupyter_api_is_served_over_the_socket(sandbox: Sandbox): + assert sandbox.commands.run(CURL_STATUS, user="root").stdout == "200" + + +def test_socket_is_not_reachable_by_user(sandbox: Sandbox): + with pytest.raises(CommandExitException) as error: + sandbox.commands.run(CURL_STATUS, user="user") + assert error.value.exit_code == 7 # curl: failed to connect + + +def test_jupyter_has_no_tcp_listener(sandbox: Sandbox): + listeners = sandbox.commands.run("ss -ltnH", user="root").stdout + assert ":8888" not in listeners + + # The kernel runs inside the sandbox, so this is the loopback view + # jupyter-server's TCP port would be exposed on. + execution = make_client(sandbox).run_code( + "import socket\nsocket.socket().connect_ex(('127.0.0.1', 8888)) != 0" + ) + assert execution.text == "True" diff --git a/packages/code-interpreter-template/tests/test_kernels.py b/packages/code-interpreter-template/tests/test_kernels.py new file mode 100644 index 0000000000..bc3893f907 --- /dev/null +++ b/packages/code-interpreter-template/tests/test_kernels.py @@ -0,0 +1,52 @@ +import pytest + +from harness import CodeInterpreter + + +def test_js_kernel(client: CodeInterpreter): + execution = client.run_code("console.log('Hello, World!')", language="js") + assert execution.stdout == ["Hello, World!\n"] + + +def test_js_esm_imports(client: CodeInterpreter): + execution = client.run_code( + """ + import { readFileSync } from 'fs' + console.log(typeof readFileSync) + """, + language="js", + ) + assert execution.stdout == ["function\n"] + + +def test_js_top_level_await(client: CodeInterpreter): + execution = client.run_code("await Promise.resolve('Hello World!')", language="js") + assert execution.text == "Hello World!" + + +@pytest.mark.skip_debug +def test_ts_kernel(client: CodeInterpreter): + execution = client.run_code( + "const message: string = 'Hello, World!'; console.log(message)", language="ts" + ) + assert execution.stdout == ["Hello, World!\n"] + + +@pytest.mark.skip_debug +def test_r_kernel(client: CodeInterpreter): + client.wait_for_kernel("r") + execution = client.run_code('print("Hello, World!")', language="r") + assert execution.stdout == ['[1] "Hello, World!"\n'] + + +@pytest.mark.skip_debug +def test_java_kernel(client: CodeInterpreter): + client.wait_for_kernel("java") + execution = client.run_code('System.out.println("Hello, World!")', language="java") + assert execution.stdout[0] == "Hello, World!" + + +@pytest.mark.skip_debug +def test_bash_kernel(client: CodeInterpreter): + execution = client.run_code("echo 'Hello, World!'", language="bash") + assert execution.stdout == ["Hello, World!\n"] diff --git a/packages/code-interpreter-template/tests/test_killed.py b/packages/code-interpreter-template/tests/test_killed.py new file mode 100644 index 0000000000..3e9d3b3b44 --- /dev/null +++ b/packages/code-interpreter-template/tests/test_killed.py @@ -0,0 +1,42 @@ +import threading +import time + +import httpx +import pytest +from e2b import Sandbox + +from conftest import make_client +from harness import Execution + + +@pytest.mark.skip_debug +def test_execution_ends_when_sandbox_is_killed(sandbox: Sandbox): + client = make_client(sandbox) + kill_error: list[BaseException] = [] + + def kill(): + try: + sandbox.kill() + except BaseException as error: # surfaced below, Timer swallows it + kill_error.append(error) + + timer = threading.Timer(2.0, kill) + timer.start() + + execution = Execution() + started = time.monotonic() + try: + # The sleep is far longer than the kill delay, so the only thing that + # can end the request is the sandbox being killed; the client timeout + # just keeps a failed kill from hanging the test. + with pytest.raises(httpx.HTTPError): + for event in client.stream_code("import time; time.sleep(300)", timeout=60): + execution.add(event) + finally: + timer.join() + client.close() + + assert not kill_error, kill_error + assert time.monotonic() - started < 60 + assert not execution.completed + assert not sandbox.is_running() diff --git a/packages/code-interpreter-template/tests/test_results.py b/packages/code-interpreter-template/tests/test_results.py new file mode 100644 index 0000000000..9faa850205 --- /dev/null +++ b/packages/code-interpreter-template/tests/test_results.py @@ -0,0 +1,111 @@ +from harness import CodeInterpreter + + +def test_display_data(client: CodeInterpreter): + execution = client.run_code( + """ + import matplotlib.pyplot as plt + import numpy as np + + x = np.linspace(0, 20, 100) + y = np.sin(x) + + plt.plot(x, y) + plt.show() + """ + ) + + result = execution.results[0] + assert result.png + assert result.text + assert not result.extra + + +def test_data(client: CodeInterpreter): + execution = client.run_code( + """ + import pandas as pd + pd.DataFrame({"a": [1, 2, 3]}) + """ + ) + + result = execution.results[0] + assert result.data + assert "a" in result.data + assert len(result.data["a"]) == 3 + + +def test_custom_repr_object(client: CodeInterpreter): + execution = client.run_code( + """ + from IPython.display import display + + display({'text/latex': r'\\text{CustomReprObject}'}, raw=True) + """ + ) + + assert execution.results[0].formats() == ["latex"] + assert execution.results[0].latex == r"\text{CustomReprObject}" + + +def test_html_result(client: CodeInterpreter): + execution = client.run_code( + """ + from IPython.display import HTML + HTML('bold') + """ + ) + + result = execution.results[0] + assert result.html == "bold" + assert result.is_main_result is True + + +def test_show_image(client: CodeInterpreter): + execution = client.run_code( + """ + import numpy + from PIL import Image + + imarray = numpy.random.rand(16,16,3) * 255 + image = Image.fromarray(imarray.astype('uint8')).convert('RGBA') + + image.show() + print("Image shown.") + """ + ) + + assert execution.results[0].png + + +def test_image_as_last_command(client: CodeInterpreter): + execution = client.run_code( + """ + import numpy + from PIL import Image + + imarray = numpy.random.rand(16,16,3) * 255 + image = Image.fromarray(imarray.astype('uint8')).convert('RGBA') + + image + """ + ) + + assert execution.results[0].png + + +def test_get_image_on_save(client: CodeInterpreter): + execution = client.run_code( + """ + import numpy + from PIL import Image + + imarray = numpy.random.rand(16,16,3) * 255 + image = Image.fromarray(imarray.astype('uint8')).convert('RGBA') + + image.save("test.png") + print("Image saved.") + """ + ) + + assert execution.results[0].png diff --git a/packages/code-interpreter-template/tests/test_streaming.py b/packages/code-interpreter-template/tests/test_streaming.py new file mode 100644 index 0000000000..c209461e83 --- /dev/null +++ b/packages/code-interpreter-template/tests/test_streaming.py @@ -0,0 +1,70 @@ +from harness import CodeInterpreter + + +def _types(events: list[dict]) -> list[str]: + return [event["type"] for event in events] + + +def test_streaming_stdout(client: CodeInterpreter): + events = list(client.stream_code("print(1)")) + + stdout = [event for event in events if event["type"] == "stdout"] + assert len(stdout) == 1 + assert stdout[0]["text"] == "1\n" + assert "timestamp" in stdout[0] + assert events[-1] == {"type": "end_of_execution"} + + +def test_streaming_stderr(client: CodeInterpreter): + events = list(client.stream_code("import sys;print(1, file=sys.stderr)")) + + stderr = [event for event in events if event["type"] == "stderr"] + assert len(stderr) == 1 + assert stderr[0]["text"] == "1\n" + + +def test_streaming_result(client: CodeInterpreter): + code = """ + import matplotlib.pyplot as plt + import numpy as np + + x = np.linspace(0, 20, 100) + y = np.sin(x) + + plt.plot(x, y) + plt.show() + + x + """ + + events = list(client.stream_code(code)) + + results = [event for event in events if event["type"] == "result"] + assert len(results) == 2 + assert results[0]["is_main_result"] is False + assert results[1]["is_main_result"] is True + + +def test_event_order(client: CodeInterpreter): + events = list(client.stream_code("print('a'); 1")) + + assert _types(events) == [ + "number_of_executions", + "stdout", + "result", + "end_of_execution", + ] + + +def test_stdout_streams_before_execution_finishes(client: CodeInterpreter): + import time + + arrival = [] + for event in client.stream_code( + "import time; print('a'); time.sleep(2); print('b')" + ): + if event["type"] == "stdout": + arrival.append((event["text"], time.monotonic())) + + assert [text for text, _ in arrival] == ["a\n", "b\n"] + assert arrival[1][1] - arrival[0][1] >= 1.5 diff --git a/packages/code-interpreter-template/tests/test_systemd.py b/packages/code-interpreter-template/tests/test_systemd.py new file mode 100644 index 0000000000..1678074591 --- /dev/null +++ b/packages/code-interpreter-template/tests/test_systemd.py @@ -0,0 +1,45 @@ +import time + +import httpx +import pytest +from e2b import Sandbox + +from conftest import make_client +from harness import CodeInterpreter + +pytestmark = pytest.mark.skip_debug + + +def wait_for_health(client: CodeInterpreter, max_retries=10, interval_ms=100) -> bool: + for _ in range(max_retries): + try: + if client.health(timeout=5).status_code == 200: + return True + except httpx.HTTPError: + pass + time.sleep(interval_ms / 1000) + return False + + +def kill_process(sandbox: Sandbox, pattern: str) -> None: + # The command handle may get killed too (killing jupyter cascades to the + # code-interpreter service), so errors are ignored. + try: + sandbox.commands.run(f"kill -9 $(pgrep -f '{pattern}')", user="root") + except Exception: + pass + + +@pytest.mark.parametrize("pattern", ["jupyter server", "uvicorn main:app"]) +def test_restart_after_kill(sandbox: Sandbox, pattern: str): + client = make_client(sandbox) + assert wait_for_health(client) + + kill_process(sandbox, pattern) + + # Wait for systemd to restart the service(s) and health to come back + assert wait_for_health(client, 60, 500) + + execution = client.run_code("x = 1; x") + assert execution.text == "1" + client.close() diff --git a/packages/code-interpreter-template/tests/uv.lock b/packages/code-interpreter-template/tests/uv.lock new file mode 100644 index 0000000000..af6f090c10 --- /dev/null +++ b/packages/code-interpreter-template/tests/uv.lock @@ -0,0 +1,602 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "bracex" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/01/5f394b8bcd6e5b92f73130990960423bbb19711f906bd9fe9ea5557c667c/bracex-3.0.1.tar.gz", hash = "sha256:4e38e32392e4a4780fe15d644bfc7c8514057cfc3861e060b11814ce829c25e4", size = 44019, upload-time = "2026-07-20T13:43:00.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/8f/6f7273a7adb8d73fc8d21ede4376a3e475e52f98435c6007f69100dec8ca/bracex-3.0.1-py3-none-any.whl", hash = "sha256:6523ad83aeb5098a4ee597cff0f964442ff74e460bd3fafaffab6a013ff2288c", size = 11940, upload-time = "2026-07-20T13:42:59.268Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "connectrpc" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf-py" }, + { name = "pyqwest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/b5/63e14ab9d4d4cc58818562db4120a35fa7454e3035633da41bdc6b712abd/connectrpc-0.11.1.tar.gz", hash = "sha256:18277f7838847b4271ca38d40c7d2387b5a2ea6a29f240689c19e1ec84aaff66", size = 46222, upload-time = "2026-07-15T06:33:31.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/ac/aa647675812cd075f2cb9acb00d62f4f2cbcbc6736049a62342b7371ce5b/connectrpc-0.11.1-py3-none-any.whl", hash = "sha256:8a52e2e92a485fa9681c1101a79a5ebeb31807e3ea3d5aabd41f484a6398bc7b", size = 64991, upload-time = "2026-07-15T06:33:29.396Z" }, +] + +[[package]] +name = "dockerfile-parse" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/df/929ee0b5d2c8bd8d713c45e71b94ab57c7e11e322130724d54f469b2cd48/dockerfile-parse-2.0.1.tar.gz", hash = "sha256:3184ccdc513221983e503ac00e1aa504a2aa8f84e5de673c46b0b6eee99ec7bc", size = 24556, upload-time = "2023-07-18T13:36:07.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/6c/79cd5bc1b880d8c1a9a5550aa8dacd57353fa3bb2457227e1fb47383eb49/dockerfile_parse-2.0.1-py2.py3-none-any.whl", hash = "sha256:bdffd126d2eb26acf1066acb54cb2e336682e1d72b974a40894fac76a4df17f6", size = 14845, upload-time = "2023-07-18T13:36:06.052Z" }, +] + +[[package]] +name = "e2b" +version = "2.53.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "connectrpc" }, + { name = "dockerfile-parse" }, + { name = "h2" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "protobuf-py" }, + { name = "pyqwest" }, + { name = "python-dateutil" }, + { name = "rich" }, + { name = "typing-extensions" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/84/a5798cbcd7e8a3d4d4b913b9caebd7ffcfd07f92e7ddef36c6972ce21f9c/e2b-2.53.1.tar.gz", hash = "sha256:c58fce7de1104f82b5402d1ec347b28535bfe34f5461ad677a0a752303f8d24b", size = 238284, upload-time = "2026-10-06T20:17:48.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/71/25fe260524bf9a7754781099ae7e30321e298d92f116373ef2e5d9e6d689/e2b-2.53.1-py3-none-any.whl", hash = "sha256:12ee0324aef2b6f064fe9037db52ddc3fcc6bec0e9c2064766b5e4a2b7c46d51", size = 400012, upload-time = "2026-10-06T20:17:46.254Z" }, +] + +[[package]] +name = "e2b-code-interpreter-tests" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "e2b" }, + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-dotenv" }, + { name = "pytest-xdist" }, + { name = "python-dotenv" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "e2b", specifier = ">=2.32.0,<3" }, + { name = "httpx", specifier = ">=0.28.1,<1" }, + { name = "pytest", specifier = ">=9.0.3,<10" }, + { name = "pytest-asyncio", specifier = ">=1.3.0,<2" }, + { name = "pytest-dotenv", specifier = ">=0.5.2,<0.6" }, + { name = "pytest-xdist", specifier = ">=3.6.1,<4" }, + { name = "python-dotenv", specifier = ">=1.2.1,<2" }, + { name = "ruff", specifier = ">=0.11.12,<0.12" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" }, +] + +[[package]] +name = "hpack" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.45.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "protobuf-py" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf-py-ext", marker = "(platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/ed/02fd902d9c51b7ff53dfc9a745eb11490722edfd30073af889e171f07b8e/protobuf_py-0.1.1.tar.gz", hash = "sha256:6bd08ac4d8f1661965bbe2685429d79043704cdd1ee720a7a89617331742240b", size = 133525, upload-time = "2026-06-24T19:02:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/00/1b3775aca1c70e3007e06ef5996f6bb9b3a32341eb0cce3ffb6effad8dec/protobuf_py-0.1.1-py3-none-any.whl", hash = "sha256:efc4f50f275ed6dae10a1f30bb81ad1a75368557b3ff22a532b7a472050368f1", size = 181656, upload-time = "2026-06-24T19:01:29.556Z" }, +] + +[[package]] +name = "protobuf-py-ext" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/05/6dc9ccff1e8159eb9a144e6d3c4acfd2211cd4fcd20c34fe9155d17d6a7f/protobuf_py_ext-0.1.1.tar.gz", hash = "sha256:e85bfdfdb3ed50634db8ccc7429dd9286520109489c735463971a418707b4fef", size = 31912, upload-time = "2026-06-24T19:02:16.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/70/2b5d62a60a2e0d88ecde1ae98db3132bcd2672fb39c8d581b82b34ac0bd8/protobuf_py_ext-0.1.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:310039e03cb15181781a0b78017419f6d4ee302e988c3c70b87f1facdf05532d", size = 306008, upload-time = "2026-06-24T19:01:31.399Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d6/73c06bb4cac2e04c0adc154965fe8b6520224bd737fd7e73bba405056321/protobuf_py_ext-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ec8348d1ba045f79b2fedd14e40cca36f2a41b52f2c4fdf55a60c58add2353", size = 314769, upload-time = "2026-06-24T19:01:32.89Z" }, + { url = "https://files.pythonhosted.org/packages/18/c5/e4e6bc6096b66d1c82639a1b501147f16ee65d32567304b987d002e2c666/protobuf_py_ext-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:182798e4861aba72d05855bd06febe4926aa7265e6f444a1b8af5252beee4f7e", size = 328122, upload-time = "2026-06-24T19:01:34.394Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/1d792a40d0f0a0f914f1dfa8bb5e9573ca0ecd5fe5cecf80d77193212abd/protobuf_py_ext-0.1.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9eb25c3a329c0551cc86b209a5e5d8ecb8d834b9924a3aa019377853a703b6d3", size = 492338, upload-time = "2026-06-24T19:01:36.103Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/5ad329223c905e5c530cae38ae24cde3584d8ab7e09457f2a01afce80e60/protobuf_py_ext-0.1.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aaecbde82bef10c7c40578cbb61b7a19896bf7fa450972050a3bb302acb7d5d6", size = 541578, upload-time = "2026-06-24T19:01:37.481Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c7/01bd8a8bfe2df4b07aafac12ccfb7b7ebe2303f65296a9e02fcafa28ff3e/protobuf_py_ext-0.1.1-cp310-abi3-win_amd64.whl", hash = "sha256:6b0c615c48e95acc53cf33e9310eeaff8b30d2d7555bf93e7bca8fb4f40e9a5c", size = 251319, upload-time = "2026-06-24T19:01:38.946Z" }, + { url = "https://files.pythonhosted.org/packages/24/dc/914065538b5db54b6a920b5af38c1ef142252ea1eea711820435fa259fac/protobuf_py_ext-0.1.1-cp310-abi3-win_arm64.whl", hash = "sha256:72956cd0af5dee24b41c6f5ba5e42622d17e6d555002b5efc1634e27a1446de2", size = 241635, upload-time = "2026-06-24T19:01:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/13/a5/0b5d73cab815fd50615cb87a02e04e8332f9e27380c890aba1459cf7e919/protobuf_py_ext-0.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f0cd58620f415a3534d195358338f4999a774e12510f91c592b38d13d37388", size = 304903, upload-time = "2026-06-24T19:01:41.796Z" }, + { url = "https://files.pythonhosted.org/packages/37/a9/14c120b9ac36a0e11bb05e482fa6ae322de583a8e1068e4859035c289947/protobuf_py_ext-0.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21572764f625d829604fc4d83635f533f35775f11c6da142345b3f3c8d64bd09", size = 316148, upload-time = "2026-06-24T19:01:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/35/54/7c00a1a9783c3ba74f6b2f5d2f049f09037bbd489c465c09a34f32fb611b/protobuf_py_ext-0.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f14f00c2678bfbe7ad46f057b9f9938c1677bcf39e405e163e272c6f9814b8", size = 326458, upload-time = "2026-06-24T19:01:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0e/81fa50c0d6c4d664e5be6d0a3c4f2c7edfef87ab12d0bac764abca1f2955/protobuf_py_ext-0.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1610865622e2e27568277ca63d8d2d23dcc55eaa767398865c21539ddf4ce24d", size = 493596, upload-time = "2026-06-24T19:01:46.344Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/3183cf6e4de62846c20549654a1d573dae51ca06aaf7fed06accdfab74f6/protobuf_py_ext-0.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8995efba9476e1ea18ef9873306871dba697308994bc2766558fec3387acc3de", size = 539656, upload-time = "2026-06-24T19:01:48.21Z" }, + { url = "https://files.pythonhosted.org/packages/a4/6c/09841f3dbe7c3d4b3f2779f8f2832ac23fb96ac8ab71674e91af732cf9ff/protobuf_py_ext-0.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba61d49dace8f874361583030a7c48139b42eb37c9ffbb1e7e8a227a51576f44", size = 305017, upload-time = "2026-06-24T19:01:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/52/43/e450b5e202f6715274acc9acd9f9769908cbdeab861a53c798fcbd467d35/protobuf_py_ext-0.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a9c2f026096ca4c595c89a297067ef371e241d3ff8e1f6d7c779aa8419cdca7", size = 316215, upload-time = "2026-06-24T19:01:51.249Z" }, + { url = "https://files.pythonhosted.org/packages/97/5c/23e79b35f1b5755b9bdc0c0c9b8cd8abababaef1a1639608d8a96bb61a9d/protobuf_py_ext-0.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf78707a040b9294e5e1ec4a1875f0046acfe52e92150cea27de4e9fc9db39bb", size = 326419, upload-time = "2026-06-24T19:01:52.93Z" }, + { url = "https://files.pythonhosted.org/packages/e8/de/5493de0ec12920a3b1dd72608ce0c1e8cdb7e0eb9e87accaecc5216df29e/protobuf_py_ext-0.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae4373845cbb85bdade3ef5368cc2f4b5f80bf173383afc1ab063f95644e5599", size = 493734, upload-time = "2026-06-24T19:01:54.301Z" }, + { url = "https://files.pythonhosted.org/packages/98/89/31da55b414e6332aad11d858e9a86bd36fbb324f52fa3fc6d8f1c57840a9/protobuf_py_ext-0.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2a4cc478eef7a2acc1daaebcde479a3ac2396d47e6bdc7e776ca4c4147ba8b4c", size = 539703, upload-time = "2026-06-24T19:01:55.707Z" }, + { url = "https://files.pythonhosted.org/packages/1c/71/39f231838ef06476d46ac40dd814894277a732a3688c0a0c994850b3b62f/protobuf_py_ext-0.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:359dccdc1c3eafed2a913c570bb082b8df848a1d27d548ce8385f246a7d68be2", size = 302145, upload-time = "2026-06-24T19:01:57.116Z" }, + { url = "https://files.pythonhosted.org/packages/87/f1/01c81ff5f420600366a0e6a613ce2af20834534d2b3d7523f4f3b4ddb54f/protobuf_py_ext-0.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91c38b4a10a306366443273ee03ca554537a0965bf05b7ada8e7dbdee04cc93e", size = 314541, upload-time = "2026-06-24T19:01:58.745Z" }, + { url = "https://files.pythonhosted.org/packages/86/d8/1cbf5a0298ceddc0cdbb28bf1ecaf8bd3cff54ed4ced6a1392d5226172fc/protobuf_py_ext-0.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79eee3bcbb289d6ea114eb8fe3a1469c5b59bf53e207238469f567d9c53ba56f", size = 325092, upload-time = "2026-06-24T19:02:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/af/23/8b1694616044cbfec2d18d4c6fffb03e05089c8bdb5970c6bafd60cc7fd5/protobuf_py_ext-0.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:10992141a8282a71ac3e3530d1a489efb27618d84000b9a47918cf70e5816d9b", size = 491684, upload-time = "2026-06-24T19:02:01.862Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/99997a7a6cf6989c944d9183c5deb6a045c27460add0316c7b34763891b3/protobuf_py_ext-0.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4411ffe0e06a774b83c5c71c546ce097640a25f596c45f95f53d3e3148e3f22d", size = 538048, upload-time = "2026-06-24T19:02:03.362Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/9e99ecbb68e1d5b46016d821eb1cbba9a91e6b50e71e75faf0c1e6189f0d/protobuf_py_ext-0.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55a17d80ea419501ff221a6627523f38a431bb33e6aa3de81ae3a7f271c49c75", size = 296337, upload-time = "2026-06-24T19:02:04.787Z" }, + { url = "https://files.pythonhosted.org/packages/af/f2/305338a28225fb54b28d6c3f5948109b9088b329a2b6f77ca610c2bdcd34/protobuf_py_ext-0.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbb518b5638403ceaa08ac4fc7dac626f45ed9b856b3517de3906cf3de4d632", size = 308961, upload-time = "2026-06-24T19:02:06.185Z" }, + { url = "https://files.pythonhosted.org/packages/60/f9/416103c93677ff2ea407704ea64fa6de9e700dc030c48360a182d91ce373/protobuf_py_ext-0.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a4adc65ab6a5e4885c67fc808bcacb83d755d05b566d312a0e10c2a873f2ad4", size = 321895, upload-time = "2026-06-24T19:02:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/5a/83/eb3e81bb2f83834b7deff4cee2d56eeb1adcbc847492a56b7f910ab257db/protobuf_py_ext-0.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e97f45c49676efacfb8e95bfcb1a002bc337e618a6780be1e55df2ba5ebd2f1e", size = 486091, upload-time = "2026-06-24T19:02:09.243Z" }, + { url = "https://files.pythonhosted.org/packages/99/96/bd88fca38556b3105e4dd41d6a176e31dcc583fe979e830aeedd2f8c20ee/protobuf_py_ext-0.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:53a6b3590f6aa7f97b8ed60f62fef9b096babfeae82f7283fd3a4c405827d4f8", size = 534582, upload-time = "2026-06-24T19:02:11.227Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyqwest" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/ee/0ff9facfa9e7a4f6df2a770d4eaf1ad0f74165da7e8c28e888461f07604c/pyqwest-0.10.0.tar.gz", hash = "sha256:6c1a693be17d57d2c2eca4085e32c2809c53090c16719a907c90ebcf1f40dc01", size = 482248, upload-time = "2026-08-21T06:09:20.656Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ee/b1a28f57c689606cfd065d8a553841150f7daaa91d20e58dcc2c5ea191f8/pyqwest-0.10.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:aa492d5777dd145a60795ed95d9d4707a3cd1091fdcdfc93a82ac7fdc43ebacd", size = 5261059, upload-time = "2026-08-21T06:08:04.999Z" }, + { url = "https://files.pythonhosted.org/packages/dc/13/9c5046cfd6ef705bde0b620ba8a794335bcabc0839342a2a647f2427b27e/pyqwest-0.10.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:59f3f16628e518c674102e7b5fcff2101bba6abb4f6737ec5fade9b9278e6a53", size = 5134207, upload-time = "2026-08-21T06:08:06.955Z" }, + { url = "https://files.pythonhosted.org/packages/93/7d/50021dd88d82d6966ab1c27593ceaee9d1ed62fbe597c40e8dc187cfa5fd/pyqwest-0.10.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6e7db305a8318b1f3218053e87501f8f245ca8bd63e948e0282d04bf0883470", size = 5640730, upload-time = "2026-08-21T06:08:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3f/5bf6c32e9e701837a8c47ce6e3ad38978cfec8eb7bc6596181e5f9e1eaeb/pyqwest-0.10.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5c757cfac5f53c8671dcb4850d5fc4c4339ea3e90636331c9318f8e3ddabc06", size = 5561462, upload-time = "2026-08-21T06:08:10.836Z" }, + { url = "https://files.pythonhosted.org/packages/5f/61/ca9ba5721461b7ce5cfaac373ab3a1723ddcc434af7430f8bd628da6e623/pyqwest-0.10.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:234b3f71e3f314d997c203d8cf829b7117edd041153f9c277d0060ab90134148", size = 5801847, upload-time = "2026-08-21T06:08:12.502Z" }, + { url = "https://files.pythonhosted.org/packages/d2/44/95593919b996a417093f598d887822b9b899e8d025588c9bfaf8c60dd812/pyqwest-0.10.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5637256a0dac0ef57e0eaa02b032014965e4a4c995e1deca1b1b97e6d1765f78", size = 5978692, upload-time = "2026-08-21T06:08:14.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/b2821ce5188457168ebb25d5ff65b1ca1bf27bc6b4a33df4bcc2357e625c/pyqwest-0.10.0-cp310-abi3-win_amd64.whl", hash = "sha256:7ea761937acf3a00d1a7e70e982949d18946e5471d1419266ab3a78bbfa19759", size = 4876627, upload-time = "2026-08-21T06:08:16.084Z" }, + { url = "https://files.pythonhosted.org/packages/86/b4/16ccef1c203fa258ce46a86aefc1a79c13b5f0b8d49627347d90eef25efd/pyqwest-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a21f1f15252a8303623b4f17b9c6de595ace11b3ade07f2adb6d07121e8191aa", size = 5274815, upload-time = "2026-08-21T06:08:17.777Z" }, + { url = "https://files.pythonhosted.org/packages/5f/61/6a87f84f571441ea43279587d4bfcad4543505918ae2b83a1ebdcfa98be5/pyqwest-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eb472c6e5d6833ebfec79db310e426eb17b01ac64c0e2c251bd9192c0d2ee0c5", size = 5123656, upload-time = "2026-08-21T06:08:19.501Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/ab69e581cf9b798b0e169f7b27fd3f8b6f9f1631bd4d3b6e22e5abaf8d8b/pyqwest-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83578e24cccd5e0dc04d60a0af7bfb43325b5f22d03ff74ff79ed0ecf553b50d", size = 5641253, upload-time = "2026-08-21T06:08:21.627Z" }, + { url = "https://files.pythonhosted.org/packages/9f/dd/f1a62eebf8321ace506bd94551a01431f6a45b882225455eae3ea6e8c6d1/pyqwest-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bb511c434f79c641efb5573e5795e56dc972252f4b96e52a9636d4ece5231a4", size = 5567341, upload-time = "2026-08-21T06:08:23.346Z" }, + { url = "https://files.pythonhosted.org/packages/4c/48/51d767691973e046887f5e6d96e32142fe296823b163ccba732233a6ef72/pyqwest-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1aaccd8a9db9430b2aedb5bad8ead80742cbc056b85c229516c70dc80539f906", size = 5803874, upload-time = "2026-08-21T06:08:25.096Z" }, + { url = "https://files.pythonhosted.org/packages/58/0a/d2834ccc6e59ad110718895cc65ff2a68aa6e010f0ba8fbe42a57ea33c21/pyqwest-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:73d9eb438ab4a957a1ce0619d3af8c1c1126bfb9181033b123d792fcf4224531", size = 5981518, upload-time = "2026-08-21T06:08:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/75/f3/274b4c268e9a55fbdb1b3637ac50b5bf42cd3a85d1cfbdc15c602a7b0d9c/pyqwest-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:317a74d633abe3bc5bccabf479e069c515dab9e6a755274b0ccb1d8a5bbfede3", size = 4870638, upload-time = "2026-08-21T06:08:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/8b1092f25159bf61a9470ebd35438c669b91ef553a7ee205bdec8006107b/pyqwest-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3978e794b9cfd8eaa500fb5d7aee63bc6172c605efa0abc1f62d85485bc049e1", size = 5273599, upload-time = "2026-08-21T06:08:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/75/10/54a9786123942b124c2afb9562b74e158afec7be40ef0caa0d37f615d379/pyqwest-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:715991fd4f04862cd7a9d7452daabcdbd74dff4dff55eb20c22d60382dc2a4ed", size = 5122920, upload-time = "2026-08-21T06:08:33.025Z" }, + { url = "https://files.pythonhosted.org/packages/a2/5b/6a6bd76f91e068b9a619f62aef9fe5ef201f859ddbb6b0a11ad3875ecdda/pyqwest-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c04798bed79c1dfa0e5b0e30fb137124311083490d44d6dfbe068d3dd254349e", size = 5639577, upload-time = "2026-08-21T06:08:34.896Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/12277a24a8dd74b0a7f124c624d9ed58eccb41e2087ff1087a14d348c778/pyqwest-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35b472877e73dd63fed089c2bc8fa198407f005c8c19e0a93f025ebefde01a81", size = 5565835, upload-time = "2026-08-21T06:08:36.567Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b6/b7da4a3f1fe43600154ae75e91ba7969024d886d60077dd3a1ba8e66d170/pyqwest-0.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:564ec360b7848b35e009038ffbca00466305a9708ab21829477f64aa8cad4c64", size = 5803078, upload-time = "2026-08-21T06:08:38.362Z" }, + { url = "https://files.pythonhosted.org/packages/0c/6f/0c9ba210f49f232289afaa8f06369c5f135ac786e1ca0cb22243b7f1fe2c/pyqwest-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5c80e88a5967c1cadb3237c450f91a84a3683f8838c8dca96f09fee3612e762", size = 5980431, upload-time = "2026-08-21T06:08:39.967Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/306eeed41a3cd3100247e6e442f4345277b70f1d24efe1641181b14839cd/pyqwest-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc3d80b402fb59dbe015e25993ac8147456fb231a4c949f92a89f31315ad50f9", size = 4870198, upload-time = "2026-08-21T06:08:41.687Z" }, + { url = "https://files.pythonhosted.org/packages/64/18/0086a408e7cbf39dab18fa5b7e42c969a98382da5a4e6debe40f05acc6a1/pyqwest-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:23a28beb55fa6d975949bffae4adfb69378f3229bb5cbd71231e95bf66f5b26c", size = 5274542, upload-time = "2026-08-21T06:08:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/e1b9aaaf7596e4faaa53cefc2efaca4e3cde721e308e6385e366361cfdcc/pyqwest-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e4415ae40b8eedb1713dab14d7f9fecc3f79d26f3206c561087b88b99d5ce24b", size = 5127922, upload-time = "2026-08-21T06:08:45.116Z" }, + { url = "https://files.pythonhosted.org/packages/75/cd/e6d68bb1de5dd26100fcfc878cbd67c402a928774edf1e8ae304c5a84f5b/pyqwest-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14b875d2273212d7fa8e4b755d8d736ffd226b1c707a9c0017dfdc8393a96eca", size = 5645856, upload-time = "2026-08-21T06:08:47.055Z" }, + { url = "https://files.pythonhosted.org/packages/f7/48/8c9f9f0467c41f6a563146d57a52f8f6d60c0cb09d0fd3ef88ad5a1c442f/pyqwest-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5071491e416ea54e3b95bf9ffbed0bd065b093cb96e10a75c3d8f2cbe3c9823", size = 5569831, upload-time = "2026-08-21T06:08:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/16/ac/c85ab70c6c72078d49a82da76b820e46aaf95a3f6fe271dac955ac195d21/pyqwest-0.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b68b5e68d513a4c63a072f8f40e38015160cf90bfbf7e8ef7c3935ca87e9e022", size = 5806735, upload-time = "2026-08-21T06:08:50.652Z" }, + { url = "https://files.pythonhosted.org/packages/66/db/ad7375b22fb2d0807431dcc9bc2aaf840e374c298cd15071024d5f6dd6d1/pyqwest-0.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c48910d27820b9c46fcd001b0fe514a3cf47d4784f59512dcdb8c91c395f82e4", size = 5984786, upload-time = "2026-08-21T06:08:52.377Z" }, + { url = "https://files.pythonhosted.org/packages/17/88/c449a772afe129683fd7acc657cbc7c69bec085dc75b6e8710a50fbb44e7/pyqwest-0.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:d03ba2cd17948b623a6210981d342eb122546d8a8e910ec77511aff4b1acdd00", size = 4872288, upload-time = "2026-08-21T06:08:54.101Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f8/439ffc0ee12cd7d9b57ac07ccea78ad3ba66b0d6817d429dd661d73308c4/pyqwest-0.10.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:07a0eb595f4096232c2d22549b6e4612c1ecada7934e46462c2c37ce14a89cfb", size = 5256823, upload-time = "2026-08-21T06:08:55.681Z" }, + { url = "https://files.pythonhosted.org/packages/df/2b/72ecd27d104d2b4284710194cce796d607674966c3ea66436768e812a66a/pyqwest-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:26401baf7dafc71c8d12d2e8389519d141e6f7c14094d0dd4cf9ec1d3b5555bd", size = 5112624, upload-time = "2026-08-21T06:08:57.366Z" }, + { url = "https://files.pythonhosted.org/packages/a4/24/0fb89c3f7d5a0410fcb7560588b4c741ef24b19195c307d4263f04e75c2b/pyqwest-0.10.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5e3c436e041d8873ce5bb0fdcf9f9e86f5604e8f0ef9e03149efebd8cb474f6", size = 5631844, upload-time = "2026-08-21T06:08:59.165Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4f/921d14754a186f0143ad62b50108dd808328e347e98e9dafab3897eeb405/pyqwest-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09364115761579eabfc79d1e954cdb3ded508dac1903fac7285d4c6f058c683f", size = 5555869, upload-time = "2026-08-21T06:09:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/b8/70/504780417319a626fe9549a7e6f9020a3d448eddf8a09617238a3426e90c/pyqwest-0.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:559674a98a8b1217e1830ecd41c9905bf2b60983c6b8017063dfac199f00727c", size = 5793738, upload-time = "2026-08-21T06:09:03.324Z" }, + { url = "https://files.pythonhosted.org/packages/45/f7/8d0a5b8a3289f4300dc9005ebde75d316f2371a2671617f942036337731a/pyqwest-0.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f399a696392fff3db3eef0a18ef65b8a3b8396d129193487d966b8eb11006376", size = 5972889, upload-time = "2026-08-21T06:09:05.2Z" }, + { url = "https://files.pythonhosted.org/packages/89/c4/f4c781e475c451cb5f4762a2f814a1750ef375b8db4db09bb2dcea03c4e5/pyqwest-0.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0f9163d6dd991bf1bf27308ba38ba021af660b15fffa47ebca98e41cf6f00309", size = 4858036, upload-time = "2026-08-21T06:09:06.926Z" }, + { url = "https://files.pythonhosted.org/packages/7a/6f/29f605665f33aab894db8daf11b3b64bdca015fb11135815c53a150191a6/pyqwest-0.10.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cfcc7ba0229baa17831582befb046ace167b368140dae022d0b89b8d586ba12c", size = 5264903, upload-time = "2026-08-21T06:09:09.183Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c8/4ce4b40f21397a6482da910fae18b32c28dcdaddf3498aef2fe38597a9e0/pyqwest-0.10.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eff9ccf427604d34c635954def07b6113d4754f968073eef2df40bdb80b05bf5", size = 5142291, upload-time = "2026-08-21T06:09:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/41/34/7205db9cddc5a2286da459ca013af629e20f09498751c60093b952379add/pyqwest-0.10.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78662158093f9d5c742368f4dd9956aa595f44c6ac0860777c98853ddd5e1610", size = 5648118, upload-time = "2026-08-21T06:09:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/1e/94/75c6cd5d01cf68c99c0cfd66b165dce3fd9d9fce97cd4973117cab68355d/pyqwest-0.10.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0879a3f0b37876372aa328b9fee956165db174c1ab72464146fe515f49399ddb", size = 5565620, upload-time = "2026-08-21T06:09:14.223Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a6/20a910d2f096908cf25ec47fa84b35b1bbb1af63dc9713f53f38ed97802e/pyqwest-0.10.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:850b8de6ade09a60bdb2f969871a177c2c304b594b2034ba3f5962c7bea75551", size = 5809262, upload-time = "2026-08-21T06:09:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f7/f92fe4004d93f08c5118e750feefdbc72a73438b7fd7083300728e999911/pyqwest-0.10.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:399802647ea646c6ac9b5460e541b7c209b7a13563c667b2690ded2060185f2e", size = 5985550, upload-time = "2026-08-21T06:09:17.579Z" }, + { url = "https://files.pythonhosted.org/packages/47/3e/2c896e54dbe3f1ba6e3bd10e9d412f422a2e33827ff591d59f67b13e0fa5/pyqwest-0.10.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c26f3de1feb5d066d7a66802a47407a93ba043696064ad80beda4a0a4bf10056", size = 4873529, upload-time = "2026-08-21T06:09:19.161Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[package]] +name = "pytest-dotenv" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/b0/cafee9c627c1bae228eb07c9977f679b3a7cb111b488307ab9594ba9e4da/pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732", size = 3782, upload-time = "2020-06-16T12:38:03.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/da/9da67c67b3d0963160e3d2cbc7c38b6fae342670cc8e6d5936644b2cf944/pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f", size = 3993, upload-time = "2020-06-16T12:38:01.139Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "wcmatch" +version = "11.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/43/30e407989e313677dbb9d5f045f966549a7254834571e342eaa4b55cc67b/wcmatch-11.0.1.tar.gz", hash = "sha256:1ea2b4fa678b8ca268253798d5963935df39132d47c3e241c0a0732224005e7d", size = 144662, upload-time = "2026-08-14T15:20:40.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/77/7a02b0f05b3ffcdbef9719ce3ee0b508d6a29b58e95299f1580055671db3/wcmatch-11.0.1-py3-none-any.whl", hash = "sha256:fd149ecddb9f0a88ea780017d6dde17c994e494e7f7303d4e3c9d6251f978f4b", size = 43449, upload-time = "2026-08-14T15:20:39.379Z" }, +] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b10802b5c..6a4449ccc4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,8 @@ importers: specifier: 'catalog:' version: 4.1.11(@types/node@20.19.43)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(vite@6.4.3(@types/node@20.19.43)(jiti@2.7.0)(terser@5.46.0)(tsx@4.23.12)(yaml@2.9.1)) + packages/charts-python: {} + packages/cli: dependencies: '@iarna/toml': @@ -213,6 +215,8 @@ importers: packages/code-interpreter-python: {} + packages/code-interpreter-template: {} + packages/desktop-js: dependencies: e2b: From dda9349058b744510947d601ae1be98d4fc3de78 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:33:55 +0000 Subject: [PATCH 2/2] refactor: move the Code Interpreter template to templates/code-interpreter Co-Authored-By: mish@e2b.dev --- .../code_interpreter_template_build.yml | 16 ++++++++-------- .../code_interpreter_template_tests.yml | 10 +++++----- .github/workflows/sdk_tests.yml | 2 +- README.md | 2 +- pnpm-lock.yaml | 4 ++-- pnpm-workspace.yaml | 1 + .../code-interpreter}/Makefile | 0 .../code-interpreter}/README.md | 2 +- .../code-interpreter}/build_ci.py | 0 .../code-interpreter}/build_debug.py | 0 .../code-interpreter}/build_docker.py | 0 .../code-interpreter}/build_prod.py | 0 .../code-interpreter}/build_test.py | 0 .../code-interpreter}/debug_logs.py | 0 .../code-interpreter}/ipython_kernel_config.py | 0 .../code-interpreter}/jupyter-healthcheck.sh | 0 .../code-interpreter}/jupyter_server_config.py | 0 .../code-interpreter}/matplotlibrc | 0 .../code-interpreter}/package.json | 0 .../code-interpreter}/requirements-dev.txt | 0 .../code-interpreter}/requirements.txt | 0 .../server/api/models/__init__.py | 0 .../server/api/models/context.py | 0 .../server/api/models/create_context.py | 0 .../server/api/models/env_vars.py | 0 .../code-interpreter}/server/api/models/error.py | 0 .../server/api/models/execution_request.py | 0 .../code-interpreter}/server/api/models/logs.py | 0 .../server/api/models/output.py | 0 .../server/api/models/result.py | 0 .../code-interpreter}/server/consts.py | 0 .../code-interpreter}/server/contexts.py | 0 .../code-interpreter}/server/envs.py | 0 .../code-interpreter}/server/errors.py | 0 .../code-interpreter}/server/main.py | 0 .../code-interpreter}/server/messaging.py | 0 .../code-interpreter}/server/requirements.txt | 0 .../code-interpreter}/server/stream.py | 0 .../code-interpreter}/server/utils/locks.py | 0 .../code-interpreter}/start-up.sh | 0 .../startup_scripts/0002_data.py | 0 .../startup_scripts/0003_images.py | 0 .../systemd/code-interpreter.service | 0 .../code-interpreter}/systemd/jupyter-debug.conf | 0 .../code-interpreter}/systemd/jupyter.service | 0 .../code-interpreter}/template.py | 0 .../code-interpreter}/tests/.env.example | 0 .../code-interpreter}/tests/README.md | 2 +- .../code-interpreter}/tests/charts/__init__.py | 0 .../code-interpreter}/tests/charts/conftest.py | 0 .../code-interpreter}/tests/charts/test_bar.py | 0 .../tests/charts/test_box_and_whiskers.py | 0 .../code-interpreter}/tests/charts/test_line.py | 0 .../tests/charts/test_log_chart.py | 0 .../code-interpreter}/tests/charts/test_pie.py | 0 .../code-interpreter}/tests/charts/test_scale.py | 0 .../tests/charts/test_scatter.py | 0 .../tests/charts/test_superchart.py | 0 .../tests/charts/test_unknown.py | 0 .../code-interpreter}/tests/conftest.py | 0 .../code-interpreter}/tests/env_vars/__init__.py | 0 .../tests/env_vars/test_bash.py | 0 .../tests/env_vars/test_java.py | 0 .../code-interpreter}/tests/env_vars/test_js.py | 0 .../tests/env_vars/test_python.py | 0 .../code-interpreter}/tests/env_vars/test_r.py | 0 .../code-interpreter}/tests/harness/__init__.py | 0 .../tests/harness/code_interpreter.py | 0 .../code-interpreter}/tests/pyproject.toml | 0 .../code-interpreter}/tests/test_async.py | 0 .../code-interpreter}/tests/test_basic.py | 0 .../code-interpreter}/tests/test_contexts.py | 0 .../code-interpreter}/tests/test_cwd.py | 0 .../code-interpreter}/tests/test_interrupt.py | 0 .../tests/test_jupyter_socket.py | 0 .../code-interpreter}/tests/test_kernels.py | 0 .../code-interpreter}/tests/test_killed.py | 0 .../code-interpreter}/tests/test_results.py | 0 .../code-interpreter}/tests/test_streaming.py | 0 .../code-interpreter}/tests/test_systemd.py | 0 .../code-interpreter}/tests/uv.lock | 0 81 files changed, 20 insertions(+), 19 deletions(-) rename {packages/code-interpreter-template => templates/code-interpreter}/Makefile (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/README.md (97%) rename {packages/code-interpreter-template => templates/code-interpreter}/build_ci.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/build_debug.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/build_docker.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/build_prod.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/build_test.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/debug_logs.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/ipython_kernel_config.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/jupyter-healthcheck.sh (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/jupyter_server_config.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/matplotlibrc (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/package.json (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/requirements-dev.txt (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/requirements.txt (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/api/models/__init__.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/api/models/context.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/api/models/create_context.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/api/models/env_vars.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/api/models/error.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/api/models/execution_request.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/api/models/logs.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/api/models/output.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/api/models/result.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/consts.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/contexts.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/envs.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/errors.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/main.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/messaging.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/requirements.txt (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/stream.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/server/utils/locks.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/start-up.sh (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/startup_scripts/0002_data.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/startup_scripts/0003_images.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/systemd/code-interpreter.service (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/systemd/jupyter-debug.conf (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/systemd/jupyter.service (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/template.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/.env.example (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/README.md (92%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/charts/__init__.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/charts/conftest.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/charts/test_bar.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/charts/test_box_and_whiskers.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/charts/test_line.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/charts/test_log_chart.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/charts/test_pie.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/charts/test_scale.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/charts/test_scatter.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/charts/test_superchart.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/charts/test_unknown.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/conftest.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/env_vars/__init__.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/env_vars/test_bash.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/env_vars/test_java.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/env_vars/test_js.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/env_vars/test_python.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/env_vars/test_r.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/harness/__init__.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/harness/code_interpreter.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/pyproject.toml (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/test_async.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/test_basic.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/test_contexts.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/test_cwd.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/test_interrupt.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/test_jupyter_socket.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/test_kernels.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/test_killed.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/test_results.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/test_streaming.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/test_systemd.py (100%) rename {packages/code-interpreter-template => templates/code-interpreter}/tests/uv.lock (100%) diff --git a/.github/workflows/code_interpreter_template_build.yml b/.github/workflows/code_interpreter_template_build.yml index 316e1a52e3..e724777e34 100644 --- a/.github/workflows/code_interpreter_template_build.yml +++ b/.github/workflows/code_interpreter_template_build.yml @@ -78,9 +78,9 @@ jobs: prefix: 'tool_version_' - name: Set package version - working-directory: ./packages/code-interpreter-template + working-directory: ./templates/code-interpreter run: | - VERSION=$(cat ../charts-python/pyproject.toml | grep version | cut -d '"' -f 2) + VERSION=$(cat ../../packages/charts-python/pyproject.toml | grep version | cut -d '"' -f 2) echo "Version: $VERSION" sed -i "s/e2b_charts/e2b_charts==${VERSION}/g" requirements.txt @@ -101,11 +101,11 @@ jobs: activate-environment: true - name: Install development dependencies - working-directory: ./packages/code-interpreter-template + working-directory: ./templates/code-interpreter run: uv pip install -r requirements-dev.txt - name: Build and push to DockerHub - working-directory: ./packages/code-interpreter-template + working-directory: ./templates/code-interpreter run: | python build_docker.py | docker buildx build \ --platform linux/amd64 \ @@ -131,9 +131,9 @@ jobs: prefix: 'tool_version_' - name: Set package version - working-directory: ./packages/code-interpreter-template + working-directory: ./templates/code-interpreter run: | - VERSION=$(cat ../charts-python/pyproject.toml | grep version | cut -d '"' -f 2) + VERSION=$(cat ../../packages/charts-python/pyproject.toml | grep version | cut -d '"' -f 2) echo "Version: $VERSION" sed -i "s/e2b_charts/e2b_charts==${VERSION}/g" requirements.txt @@ -145,7 +145,7 @@ jobs: activate-environment: true - name: Install development dependencies - working-directory: ./packages/code-interpreter-template + working-directory: ./templates/code-interpreter run: uv pip install -r requirements-dev.txt - name: Resolve target environment @@ -203,7 +203,7 @@ jobs: - name: Build E2B template id: build-template - working-directory: ./packages/code-interpreter-template + working-directory: ./templates/code-interpreter run: python build_prod.py env: SKIP_CACHE: ${{ inputs.skip_cache }} diff --git a/.github/workflows/code_interpreter_template_tests.yml b/.github/workflows/code_interpreter_template_tests.yml index b3781b27fc..d909515d31 100644 --- a/.github/workflows/code_interpreter_template_tests.yml +++ b/.github/workflows/code_interpreter_template_tests.yml @@ -40,9 +40,9 @@ jobs: prefix: 'tool_version_' - name: Set package version - working-directory: ./packages/code-interpreter-template + working-directory: ./templates/code-interpreter run: | - VERSION=$(cat ../charts-python/pyproject.toml | grep version | cut -d '"' -f 2) + VERSION=$(cat ../../packages/charts-python/pyproject.toml | grep version | cut -d '"' -f 2) echo "Version: $VERSION" sed -i "s/e2b_charts/e2b_charts==${VERSION}/g" requirements.txt @@ -54,7 +54,7 @@ jobs: activate-environment: true - name: Install development dependencies - working-directory: ./packages/code-interpreter-template + working-directory: ./templates/code-interpreter run: uv pip install -r requirements-dev.txt - name: Generate Template Name @@ -66,7 +66,7 @@ jobs: - name: Build E2B template id: build-template - working-directory: ./packages/code-interpreter-template + working-directory: ./templates/code-interpreter run: python build_ci.py env: E2B_API_KEY: ${{ secrets.E2B_API_KEY }} @@ -77,7 +77,7 @@ jobs: needs: [build] defaults: run: - working-directory: ./packages/code-interpreter-template/tests + working-directory: ./templates/code-interpreter/tests shell: bash name: Code Interpreter - HTTP tests runs-on: ubuntu-22.04 diff --git a/.github/workflows/sdk_tests.yml b/.github/workflows/sdk_tests.yml index 6b3e162a9f..a764fe8797 100644 --- a/.github/workflows/sdk_tests.yml +++ b/.github/workflows/sdk_tests.yml @@ -92,7 +92,7 @@ jobs: - '.github/workflows/sdk_tests.yml' - '.github/workflows/code_interpreter_template_tests.yml' - '.tool-versions' - - 'packages/code-interpreter-template/**/!(*.md)' + - 'templates/code-interpreter/**/!(*.md)' - 'packages/charts-python/pyproject.toml' charts_python: - '.github/workflows/sdk_tests.yml' diff --git a/README.md b/README.md index 037555a912..ada2111ea5 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ npm i @e2b/code-interpreter # JavaScript/TypeScript pip install e2b-code-interpreter # Python ``` -The [`@e2b/code-interpreter-template`](./packages/code-interpreter-template) +The [`@e2b/code-interpreter-template`](./templates/code-interpreter) and [`@e2b/charts-python`](./packages/charts-python) packages provide the Code Interpreter sandbox template and Python chart extraction package. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a4449ccc4..290e61e474 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -215,8 +215,6 @@ importers: packages/code-interpreter-python: {} - packages/code-interpreter-template: {} - packages/desktop-js: dependencies: e2b: @@ -371,6 +369,8 @@ importers: specifier: workspace:* version: link:../dockerfile-utils-python + templates/code-interpreter: {} + packages: '@apidevtools/json-schema-ref-parser@11.9.3': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 14b3252768..10fda4d2e4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - packages/* + - templates/* onlyBuiltDependencies: - esbuild diff --git a/packages/code-interpreter-template/Makefile b/templates/code-interpreter/Makefile similarity index 100% rename from packages/code-interpreter-template/Makefile rename to templates/code-interpreter/Makefile diff --git a/packages/code-interpreter-template/README.md b/templates/code-interpreter/README.md similarity index 97% rename from packages/code-interpreter-template/README.md rename to templates/code-interpreter/README.md index bf1e850550..ffce81c3c9 100644 --- a/packages/code-interpreter-template/README.md +++ b/templates/code-interpreter/README.md @@ -1,6 +1,6 @@ # Code Interpreter -Run the commands below from `packages/code-interpreter-template`. +Run the commands below from `templates/code-interpreter`. ## Building the production template diff --git a/packages/code-interpreter-template/build_ci.py b/templates/code-interpreter/build_ci.py similarity index 100% rename from packages/code-interpreter-template/build_ci.py rename to templates/code-interpreter/build_ci.py diff --git a/packages/code-interpreter-template/build_debug.py b/templates/code-interpreter/build_debug.py similarity index 100% rename from packages/code-interpreter-template/build_debug.py rename to templates/code-interpreter/build_debug.py diff --git a/packages/code-interpreter-template/build_docker.py b/templates/code-interpreter/build_docker.py similarity index 100% rename from packages/code-interpreter-template/build_docker.py rename to templates/code-interpreter/build_docker.py diff --git a/packages/code-interpreter-template/build_prod.py b/templates/code-interpreter/build_prod.py similarity index 100% rename from packages/code-interpreter-template/build_prod.py rename to templates/code-interpreter/build_prod.py diff --git a/packages/code-interpreter-template/build_test.py b/templates/code-interpreter/build_test.py similarity index 100% rename from packages/code-interpreter-template/build_test.py rename to templates/code-interpreter/build_test.py diff --git a/packages/code-interpreter-template/debug_logs.py b/templates/code-interpreter/debug_logs.py similarity index 100% rename from packages/code-interpreter-template/debug_logs.py rename to templates/code-interpreter/debug_logs.py diff --git a/packages/code-interpreter-template/ipython_kernel_config.py b/templates/code-interpreter/ipython_kernel_config.py similarity index 100% rename from packages/code-interpreter-template/ipython_kernel_config.py rename to templates/code-interpreter/ipython_kernel_config.py diff --git a/packages/code-interpreter-template/jupyter-healthcheck.sh b/templates/code-interpreter/jupyter-healthcheck.sh similarity index 100% rename from packages/code-interpreter-template/jupyter-healthcheck.sh rename to templates/code-interpreter/jupyter-healthcheck.sh diff --git a/packages/code-interpreter-template/jupyter_server_config.py b/templates/code-interpreter/jupyter_server_config.py similarity index 100% rename from packages/code-interpreter-template/jupyter_server_config.py rename to templates/code-interpreter/jupyter_server_config.py diff --git a/packages/code-interpreter-template/matplotlibrc b/templates/code-interpreter/matplotlibrc similarity index 100% rename from packages/code-interpreter-template/matplotlibrc rename to templates/code-interpreter/matplotlibrc diff --git a/packages/code-interpreter-template/package.json b/templates/code-interpreter/package.json similarity index 100% rename from packages/code-interpreter-template/package.json rename to templates/code-interpreter/package.json diff --git a/packages/code-interpreter-template/requirements-dev.txt b/templates/code-interpreter/requirements-dev.txt similarity index 100% rename from packages/code-interpreter-template/requirements-dev.txt rename to templates/code-interpreter/requirements-dev.txt diff --git a/packages/code-interpreter-template/requirements.txt b/templates/code-interpreter/requirements.txt similarity index 100% rename from packages/code-interpreter-template/requirements.txt rename to templates/code-interpreter/requirements.txt diff --git a/packages/code-interpreter-template/server/api/models/__init__.py b/templates/code-interpreter/server/api/models/__init__.py similarity index 100% rename from packages/code-interpreter-template/server/api/models/__init__.py rename to templates/code-interpreter/server/api/models/__init__.py diff --git a/packages/code-interpreter-template/server/api/models/context.py b/templates/code-interpreter/server/api/models/context.py similarity index 100% rename from packages/code-interpreter-template/server/api/models/context.py rename to templates/code-interpreter/server/api/models/context.py diff --git a/packages/code-interpreter-template/server/api/models/create_context.py b/templates/code-interpreter/server/api/models/create_context.py similarity index 100% rename from packages/code-interpreter-template/server/api/models/create_context.py rename to templates/code-interpreter/server/api/models/create_context.py diff --git a/packages/code-interpreter-template/server/api/models/env_vars.py b/templates/code-interpreter/server/api/models/env_vars.py similarity index 100% rename from packages/code-interpreter-template/server/api/models/env_vars.py rename to templates/code-interpreter/server/api/models/env_vars.py diff --git a/packages/code-interpreter-template/server/api/models/error.py b/templates/code-interpreter/server/api/models/error.py similarity index 100% rename from packages/code-interpreter-template/server/api/models/error.py rename to templates/code-interpreter/server/api/models/error.py diff --git a/packages/code-interpreter-template/server/api/models/execution_request.py b/templates/code-interpreter/server/api/models/execution_request.py similarity index 100% rename from packages/code-interpreter-template/server/api/models/execution_request.py rename to templates/code-interpreter/server/api/models/execution_request.py diff --git a/packages/code-interpreter-template/server/api/models/logs.py b/templates/code-interpreter/server/api/models/logs.py similarity index 100% rename from packages/code-interpreter-template/server/api/models/logs.py rename to templates/code-interpreter/server/api/models/logs.py diff --git a/packages/code-interpreter-template/server/api/models/output.py b/templates/code-interpreter/server/api/models/output.py similarity index 100% rename from packages/code-interpreter-template/server/api/models/output.py rename to templates/code-interpreter/server/api/models/output.py diff --git a/packages/code-interpreter-template/server/api/models/result.py b/templates/code-interpreter/server/api/models/result.py similarity index 100% rename from packages/code-interpreter-template/server/api/models/result.py rename to templates/code-interpreter/server/api/models/result.py diff --git a/packages/code-interpreter-template/server/consts.py b/templates/code-interpreter/server/consts.py similarity index 100% rename from packages/code-interpreter-template/server/consts.py rename to templates/code-interpreter/server/consts.py diff --git a/packages/code-interpreter-template/server/contexts.py b/templates/code-interpreter/server/contexts.py similarity index 100% rename from packages/code-interpreter-template/server/contexts.py rename to templates/code-interpreter/server/contexts.py diff --git a/packages/code-interpreter-template/server/envs.py b/templates/code-interpreter/server/envs.py similarity index 100% rename from packages/code-interpreter-template/server/envs.py rename to templates/code-interpreter/server/envs.py diff --git a/packages/code-interpreter-template/server/errors.py b/templates/code-interpreter/server/errors.py similarity index 100% rename from packages/code-interpreter-template/server/errors.py rename to templates/code-interpreter/server/errors.py diff --git a/packages/code-interpreter-template/server/main.py b/templates/code-interpreter/server/main.py similarity index 100% rename from packages/code-interpreter-template/server/main.py rename to templates/code-interpreter/server/main.py diff --git a/packages/code-interpreter-template/server/messaging.py b/templates/code-interpreter/server/messaging.py similarity index 100% rename from packages/code-interpreter-template/server/messaging.py rename to templates/code-interpreter/server/messaging.py diff --git a/packages/code-interpreter-template/server/requirements.txt b/templates/code-interpreter/server/requirements.txt similarity index 100% rename from packages/code-interpreter-template/server/requirements.txt rename to templates/code-interpreter/server/requirements.txt diff --git a/packages/code-interpreter-template/server/stream.py b/templates/code-interpreter/server/stream.py similarity index 100% rename from packages/code-interpreter-template/server/stream.py rename to templates/code-interpreter/server/stream.py diff --git a/packages/code-interpreter-template/server/utils/locks.py b/templates/code-interpreter/server/utils/locks.py similarity index 100% rename from packages/code-interpreter-template/server/utils/locks.py rename to templates/code-interpreter/server/utils/locks.py diff --git a/packages/code-interpreter-template/start-up.sh b/templates/code-interpreter/start-up.sh similarity index 100% rename from packages/code-interpreter-template/start-up.sh rename to templates/code-interpreter/start-up.sh diff --git a/packages/code-interpreter-template/startup_scripts/0002_data.py b/templates/code-interpreter/startup_scripts/0002_data.py similarity index 100% rename from packages/code-interpreter-template/startup_scripts/0002_data.py rename to templates/code-interpreter/startup_scripts/0002_data.py diff --git a/packages/code-interpreter-template/startup_scripts/0003_images.py b/templates/code-interpreter/startup_scripts/0003_images.py similarity index 100% rename from packages/code-interpreter-template/startup_scripts/0003_images.py rename to templates/code-interpreter/startup_scripts/0003_images.py diff --git a/packages/code-interpreter-template/systemd/code-interpreter.service b/templates/code-interpreter/systemd/code-interpreter.service similarity index 100% rename from packages/code-interpreter-template/systemd/code-interpreter.service rename to templates/code-interpreter/systemd/code-interpreter.service diff --git a/packages/code-interpreter-template/systemd/jupyter-debug.conf b/templates/code-interpreter/systemd/jupyter-debug.conf similarity index 100% rename from packages/code-interpreter-template/systemd/jupyter-debug.conf rename to templates/code-interpreter/systemd/jupyter-debug.conf diff --git a/packages/code-interpreter-template/systemd/jupyter.service b/templates/code-interpreter/systemd/jupyter.service similarity index 100% rename from packages/code-interpreter-template/systemd/jupyter.service rename to templates/code-interpreter/systemd/jupyter.service diff --git a/packages/code-interpreter-template/template.py b/templates/code-interpreter/template.py similarity index 100% rename from packages/code-interpreter-template/template.py rename to templates/code-interpreter/template.py diff --git a/packages/code-interpreter-template/tests/.env.example b/templates/code-interpreter/tests/.env.example similarity index 100% rename from packages/code-interpreter-template/tests/.env.example rename to templates/code-interpreter/tests/.env.example diff --git a/packages/code-interpreter-template/tests/README.md b/templates/code-interpreter/tests/README.md similarity index 92% rename from packages/code-interpreter-template/tests/README.md rename to templates/code-interpreter/tests/README.md index 3632293e38..ac10d19145 100644 --- a/packages/code-interpreter-template/tests/README.md +++ b/templates/code-interpreter/tests/README.md @@ -1,6 +1,6 @@ # Code Interpreter HTTP tests -Run the commands below from `packages/code-interpreter-template/tests`. +Run the commands below from `templates/code-interpreter/tests`. End-to-end tests for the Code Interpreter template. Sandboxes are created with the `e2b` SDK from `E2B_TESTS_TEMPLATE` (defaults to `code-interpreter-v1`); diff --git a/packages/code-interpreter-template/tests/charts/__init__.py b/templates/code-interpreter/tests/charts/__init__.py similarity index 100% rename from packages/code-interpreter-template/tests/charts/__init__.py rename to templates/code-interpreter/tests/charts/__init__.py diff --git a/packages/code-interpreter-template/tests/charts/conftest.py b/templates/code-interpreter/tests/charts/conftest.py similarity index 100% rename from packages/code-interpreter-template/tests/charts/conftest.py rename to templates/code-interpreter/tests/charts/conftest.py diff --git a/packages/code-interpreter-template/tests/charts/test_bar.py b/templates/code-interpreter/tests/charts/test_bar.py similarity index 100% rename from packages/code-interpreter-template/tests/charts/test_bar.py rename to templates/code-interpreter/tests/charts/test_bar.py diff --git a/packages/code-interpreter-template/tests/charts/test_box_and_whiskers.py b/templates/code-interpreter/tests/charts/test_box_and_whiskers.py similarity index 100% rename from packages/code-interpreter-template/tests/charts/test_box_and_whiskers.py rename to templates/code-interpreter/tests/charts/test_box_and_whiskers.py diff --git a/packages/code-interpreter-template/tests/charts/test_line.py b/templates/code-interpreter/tests/charts/test_line.py similarity index 100% rename from packages/code-interpreter-template/tests/charts/test_line.py rename to templates/code-interpreter/tests/charts/test_line.py diff --git a/packages/code-interpreter-template/tests/charts/test_log_chart.py b/templates/code-interpreter/tests/charts/test_log_chart.py similarity index 100% rename from packages/code-interpreter-template/tests/charts/test_log_chart.py rename to templates/code-interpreter/tests/charts/test_log_chart.py diff --git a/packages/code-interpreter-template/tests/charts/test_pie.py b/templates/code-interpreter/tests/charts/test_pie.py similarity index 100% rename from packages/code-interpreter-template/tests/charts/test_pie.py rename to templates/code-interpreter/tests/charts/test_pie.py diff --git a/packages/code-interpreter-template/tests/charts/test_scale.py b/templates/code-interpreter/tests/charts/test_scale.py similarity index 100% rename from packages/code-interpreter-template/tests/charts/test_scale.py rename to templates/code-interpreter/tests/charts/test_scale.py diff --git a/packages/code-interpreter-template/tests/charts/test_scatter.py b/templates/code-interpreter/tests/charts/test_scatter.py similarity index 100% rename from packages/code-interpreter-template/tests/charts/test_scatter.py rename to templates/code-interpreter/tests/charts/test_scatter.py diff --git a/packages/code-interpreter-template/tests/charts/test_superchart.py b/templates/code-interpreter/tests/charts/test_superchart.py similarity index 100% rename from packages/code-interpreter-template/tests/charts/test_superchart.py rename to templates/code-interpreter/tests/charts/test_superchart.py diff --git a/packages/code-interpreter-template/tests/charts/test_unknown.py b/templates/code-interpreter/tests/charts/test_unknown.py similarity index 100% rename from packages/code-interpreter-template/tests/charts/test_unknown.py rename to templates/code-interpreter/tests/charts/test_unknown.py diff --git a/packages/code-interpreter-template/tests/conftest.py b/templates/code-interpreter/tests/conftest.py similarity index 100% rename from packages/code-interpreter-template/tests/conftest.py rename to templates/code-interpreter/tests/conftest.py diff --git a/packages/code-interpreter-template/tests/env_vars/__init__.py b/templates/code-interpreter/tests/env_vars/__init__.py similarity index 100% rename from packages/code-interpreter-template/tests/env_vars/__init__.py rename to templates/code-interpreter/tests/env_vars/__init__.py diff --git a/packages/code-interpreter-template/tests/env_vars/test_bash.py b/templates/code-interpreter/tests/env_vars/test_bash.py similarity index 100% rename from packages/code-interpreter-template/tests/env_vars/test_bash.py rename to templates/code-interpreter/tests/env_vars/test_bash.py diff --git a/packages/code-interpreter-template/tests/env_vars/test_java.py b/templates/code-interpreter/tests/env_vars/test_java.py similarity index 100% rename from packages/code-interpreter-template/tests/env_vars/test_java.py rename to templates/code-interpreter/tests/env_vars/test_java.py diff --git a/packages/code-interpreter-template/tests/env_vars/test_js.py b/templates/code-interpreter/tests/env_vars/test_js.py similarity index 100% rename from packages/code-interpreter-template/tests/env_vars/test_js.py rename to templates/code-interpreter/tests/env_vars/test_js.py diff --git a/packages/code-interpreter-template/tests/env_vars/test_python.py b/templates/code-interpreter/tests/env_vars/test_python.py similarity index 100% rename from packages/code-interpreter-template/tests/env_vars/test_python.py rename to templates/code-interpreter/tests/env_vars/test_python.py diff --git a/packages/code-interpreter-template/tests/env_vars/test_r.py b/templates/code-interpreter/tests/env_vars/test_r.py similarity index 100% rename from packages/code-interpreter-template/tests/env_vars/test_r.py rename to templates/code-interpreter/tests/env_vars/test_r.py diff --git a/packages/code-interpreter-template/tests/harness/__init__.py b/templates/code-interpreter/tests/harness/__init__.py similarity index 100% rename from packages/code-interpreter-template/tests/harness/__init__.py rename to templates/code-interpreter/tests/harness/__init__.py diff --git a/packages/code-interpreter-template/tests/harness/code_interpreter.py b/templates/code-interpreter/tests/harness/code_interpreter.py similarity index 100% rename from packages/code-interpreter-template/tests/harness/code_interpreter.py rename to templates/code-interpreter/tests/harness/code_interpreter.py diff --git a/packages/code-interpreter-template/tests/pyproject.toml b/templates/code-interpreter/tests/pyproject.toml similarity index 100% rename from packages/code-interpreter-template/tests/pyproject.toml rename to templates/code-interpreter/tests/pyproject.toml diff --git a/packages/code-interpreter-template/tests/test_async.py b/templates/code-interpreter/tests/test_async.py similarity index 100% rename from packages/code-interpreter-template/tests/test_async.py rename to templates/code-interpreter/tests/test_async.py diff --git a/packages/code-interpreter-template/tests/test_basic.py b/templates/code-interpreter/tests/test_basic.py similarity index 100% rename from packages/code-interpreter-template/tests/test_basic.py rename to templates/code-interpreter/tests/test_basic.py diff --git a/packages/code-interpreter-template/tests/test_contexts.py b/templates/code-interpreter/tests/test_contexts.py similarity index 100% rename from packages/code-interpreter-template/tests/test_contexts.py rename to templates/code-interpreter/tests/test_contexts.py diff --git a/packages/code-interpreter-template/tests/test_cwd.py b/templates/code-interpreter/tests/test_cwd.py similarity index 100% rename from packages/code-interpreter-template/tests/test_cwd.py rename to templates/code-interpreter/tests/test_cwd.py diff --git a/packages/code-interpreter-template/tests/test_interrupt.py b/templates/code-interpreter/tests/test_interrupt.py similarity index 100% rename from packages/code-interpreter-template/tests/test_interrupt.py rename to templates/code-interpreter/tests/test_interrupt.py diff --git a/packages/code-interpreter-template/tests/test_jupyter_socket.py b/templates/code-interpreter/tests/test_jupyter_socket.py similarity index 100% rename from packages/code-interpreter-template/tests/test_jupyter_socket.py rename to templates/code-interpreter/tests/test_jupyter_socket.py diff --git a/packages/code-interpreter-template/tests/test_kernels.py b/templates/code-interpreter/tests/test_kernels.py similarity index 100% rename from packages/code-interpreter-template/tests/test_kernels.py rename to templates/code-interpreter/tests/test_kernels.py diff --git a/packages/code-interpreter-template/tests/test_killed.py b/templates/code-interpreter/tests/test_killed.py similarity index 100% rename from packages/code-interpreter-template/tests/test_killed.py rename to templates/code-interpreter/tests/test_killed.py diff --git a/packages/code-interpreter-template/tests/test_results.py b/templates/code-interpreter/tests/test_results.py similarity index 100% rename from packages/code-interpreter-template/tests/test_results.py rename to templates/code-interpreter/tests/test_results.py diff --git a/packages/code-interpreter-template/tests/test_streaming.py b/templates/code-interpreter/tests/test_streaming.py similarity index 100% rename from packages/code-interpreter-template/tests/test_streaming.py rename to templates/code-interpreter/tests/test_streaming.py diff --git a/packages/code-interpreter-template/tests/test_systemd.py b/templates/code-interpreter/tests/test_systemd.py similarity index 100% rename from packages/code-interpreter-template/tests/test_systemd.py rename to templates/code-interpreter/tests/test_systemd.py diff --git a/packages/code-interpreter-template/tests/uv.lock b/templates/code-interpreter/tests/uv.lock similarity index 100% rename from packages/code-interpreter-template/tests/uv.lock rename to templates/code-interpreter/tests/uv.lock