diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index b59f3a2..dd50232 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -49,9 +49,42 @@ jobs: IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" echo "IMAGE_LOWERCASE=$(echo $IMAGE | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + test: + strategy: + fail-fast: false + # This job runs on two different runners: one for x86_64 and one for ARM. + # The ARM runner is used to test the image on an ARM architecture. + # The x86_64 runner is used to test the image on an x86_64 architecture. + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-24.04 + - platform: linux/arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + + - name: Install container-structure-test + run: | + if [[ "${{ matrix.platform }}" == *"arm64"* ]]; then + curl -LO https://github.com/GoogleContainerTools/container-structure-test/releases/latest/download/container-structure-test-linux-arm64 && chmod +x container-structure-test-linux-arm64 && sudo mv container-structure-test-linux-arm64 /usr/local/bin/container-structure-test + else + curl -LO https://github.com/GoogleContainerTools/container-structure-test/releases/latest/download/container-structure-test-linux-amd64 && chmod +x container-structure-test-linux-amd64 && sudo mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test + fi + + - name: Run tests + run: | + ./test.sh ${{ matrix.platform }} + build_and_push_image: needs: - prep + - test env: IMAGE_LOWERCASE: ${{ needs.prep.outputs.IMAGE_LOWERCASE }} strategy: diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..534704c --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,12 @@ +{ + "recommendations": [ + "timonwong.shellcheck", + "ms-azuretools.vscode-docker", + "exiasr.hadolint", + "foxundermoon.shell-format", + "davidanson.vscode-markdownlint", + "github.vscode-github-actions", + "redhat.vscode-yaml", + "docker.docker" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..268d16e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "[shellscript]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[dockerfile]": { + "editor.defaultFormatter": "ms-azuretools.vscode-docker" + }, + "[markdown]": { + "editor.defaultFormatter": "DavidAnson.vscode-markdownlint" + }, + "editor.formatOnSave": true, + "yaml.format.enable": true, +} \ No newline at end of file diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..7e9d01f --- /dev/null +++ b/Brewfile @@ -0,0 +1,5 @@ +tap "homebrew/bundle" +brew "container-structure-test" +brew "hadolint" +brew "shellcheck" +brew "shfmt" diff --git a/Dockerfile b/Dockerfile index 162779d..dffbf95 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,67 +1,73 @@ -FROM python:3.12-bookworm +FROM ubuntu:24.04 AS base ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 ENV JAVA_HOME=/usr/lib/jvm/java-openjdk +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + # https://docs.docker.com/build/cache/optimize/#use-cache-mounts -RUN --mount=type=cache,target=/var/cache/apt \ - --mount=type=cache,target=/var/lib/apt \ - --mount=type=cache,target=/root/.cache/pip \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update \ + && apt-get upgrade -y \ && apt-get install -y --no-install-recommends --no-install-suggests \ - # Required for pyre vscode extension - watchman \ - # Required for sonarqube vscode extension - openjdk-17-jre-headless \ - nodejs \ - # Required for shellcheck vscode extension - shellcheck \ - # Required for general purpose compilation - gcc \ - # General purpose tools - curl \ - git \ - jq \ - zsh \ - && pip install --no-cache-dir -U pip setuptools wheel \ - && pip install --no-cache-dir uv \ + # Required for pyre vscode extension + watchman \ + # Required for sonarqube vscode extension + openjdk-17-jre-headless \ + nodejs \ + # Required for shellcheck vscode extension + shellcheck \ + # Required for general purpose compilation + gcc \ + # General purpose tools + curl \ + git \ + jq \ + zsh \ + # Install uv: + && curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/local/bin" sh \ # Install Pulumi: && curl -fsSL https://get.pulumi.com | sh \ + && mv /root/.pulumi/bin/pulumi /usr/local/bin \ # Install reviewdog: && curl -sfL https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh \ - | sh -s -- -b /usr/local/bin \ + | sh -s -- -b /usr/local/bin \ # Make sure java runtime is found for sonarqube: && ln -s "$(dirname "$(dirname "$(readlink -f "$(which java)")")")" "$JAVA_HOME" \ # Install other tools: && export ACTIONLINT_VERSION=$(curl -s https://api.github.com/repos/rhysd/actionlint/releases/latest | jq -r '.tag_name' | sed "s/v//") \ && export HADOLINT_VERSION=$(curl -s https://api.github.com/repos/hadolint/hadolint/releases/latest | jq -r '.tag_name') \ + && export SHFMT_VERSION=$(curl -s https://api.github.com/repos/mvdan/sh/releases/latest | jq -r '.tag_name') \ && if [ "$(uname -m)" = "aarch64" ]; then \ - curl -o /usr/local/bin/snyk -L https://static.snyk.io/cli/latest/snyk-linux-arm64 \ - && curl -o /usr/local/bin/hadolint -L https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-Linux-arm64 \ - && curl -o /usr/local/bin/shfmt https://github.com/patrickvane/shfmt/releases/download/master/shfmt_linux_arm \ - && curl -sL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_arm64.tar.gz" | tar -xzf - -C /usr/local/bin actionlint ; \ + curl -o /usr/local/bin/snyk -L https://static.snyk.io/cli/latest/snyk-linux-arm64 \ + && curl -o /usr/local/bin/hadolint -L https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-Linux-arm64 \ + && curl -o /usr/local/bin/shfmt -L https://github.com/mvdan/sh/releases/download/${SHFMT_VERSION}/shfmt_${SHFMT_VERSION}_linux_arm64 \ + && curl -sL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_arm64.tar.gz" | tar -xzf - -C /usr/local/bin actionlint ; \ else \ - curl -o /usr/local/bin/snyk -L https://static.snyk.io/cli/latest/snyk-linux \ - && curl -o /usr/local/bin/hadolint -L https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-Linux-x86_64 \ - && curl -o /usr/local/bin/shfmt https://github.com/patrickvane/shfmt/releases/download/master/shfmt_linux_amd64 \ - && curl -sL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" | tar -xzf - -C /usr/local/bin actionlint ; \ + curl -o /usr/local/bin/snyk -L https://static.snyk.io/cli/latest/snyk-linux \ + && curl -o /usr/local/bin/hadolint -L https://github.com/hadolint/hadolint/releases/download/${HADOLINT_VERSION}/hadolint-Linux-x86_64 \ + && curl -o /usr/local/bin/shfmt -L https://github.com/mvdan/sh/releases/download/${SHFMT_VERSION}/shfmt_${SHFMT_VERSION}_linux_amd64 \ + && curl -sL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" | tar -xzf - -C /usr/local/bin actionlint ; \ fi \ && chmod +x /usr/local/bin/snyk \ && chmod +x /usr/local/bin/hadolint \ && chmod +x /usr/local/bin/shfmt \ && chmod +x /usr/local/bin/actionlint +WORKDIR /app +ENV PATH="/opt/venv/bin:$PATH" # Copy from the cache instead of linking since it's a mounted volume ENV UV_LINK_MODE=copy -ENV UV_SYSTEM_PYTHON=true -ENV UV_BREAK_SYSTEM_PACKAGES=true -ENV UV_PROJECT_ENVIRONMENT=/usr/local - +ENV UV_PROJECT_ENVIRONMENT=/opt/venv +ENV UV_PYTHON_INSTALL_DIR=/opt/pythons # Install the project's dependencies using the lockfile and settings ONBUILD RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --frozen --no-install-project + --mount=type=bind,source=uv.lock,target=uv.lock,readonly \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml,readonly \ + --mount=type=bind,source=.python-version,target=.python-version,readonly \ + uv venv \ + && uv sync --frozen --no-install-project diff --git a/README.md b/README.md index cf1b3fe..c55b138 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,83 @@ # Python base image for development purpose -Specially created Docker image for Python to work as a devcontainer for development purposes. +Specially created Docker image for Python to work as a devcontainer for development purposes. + +NOTE: This image is NOT meant to be used as a production image. ## Contains It contains: -- `python` - Python sourced from official Python image on Docker Hub. We are using now Python version 3.12 as it is now the latest Python version supported by the type checkers we use. - `uv` - for Python package management +- `python` - Python version that is defined in `.python-version` file in the child image. -It contains the neccesary dependencies for running various linters and type checkers: +It contains the necessary dependencies for running various linters and type checkers: -- `watchman` - for running Pyre Python type checker +- `watchman` - for running Pyre Python type checker - `shfmt` - for shell script formatting +- `shellcheck` - for shell script linting - `reviewdog` - for code review - `hadolint` - for linting Dockerfile - `actionlint` - static checker for GitHub Actions workflow files +Other tools: + +- `pulumi` - Pulumi CLI for infrastructure as code + ## Usage We host the image on [Github packages](https://github.com/NextGenContributions/python-dev-image/pkgs/container/python-dev-image). You can use it the like this: +### As a standalone image + Command line: + ```shell -docker pull ghcr.io/nextgencontributions/python-dev-image +docker run --rm ghcr.io/nextgencontributions/python-dev-image ``` +### As a base image + In your project's `Dockerfile`: + ```Dockerfile -FROM ghcr.io/nextgencontributions/python-dev-image +FROM ghcr.io/nextgencontributions/python-dev-image # AS scratch # Do your own customizations here... ``` +NOTE: When using this as a base image, you should have the following files available in your build context: + +- `pyproject.toml` +- `uv.lock` +- `.python-version` - this will be used to install the Python version in the container. + +If you don't have these files, you can create them by running the following commands: + +```shell +uv init +uv lock +uv python pin 3.12 # or replace with any other Python version you want +``` + +Or in a single command in your project's root directory: + +```shell +docker run --rm -v $(pwd):/app ghcr.io/nextgencontributions/python-dev-image \ + sh -c "cd /app && uv init && uv lock && uv python pin 3.12" +``` + +### As a devcontainer + With VSCode in `.devcontainer/devcontainer.json`: + ```jsonc // For format details, see https://aka.ms/devcontainer.json. { - "name": "Python 3", - "image": "ghcr.io/nextgencontributions/python-dev-image" - // Do your own customizations here... + "name": "Python 3", + "image": "ghcr.io/nextgencontributions/python-dev-image" + // Do your own customizations here... } ``` diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..977892c --- /dev/null +++ b/test.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# This script builds and tests a Docker image for multiple platforms (amd64 and arm64). +# It uses Docker Buildx to build the image and container-structure-test to run tests on the built image. +# +# Usage: ./test.sh [platform] +# +# If no platform is specified, it defaults to "linux/amd64 linux/arm64". + +set -euxo pipefail + +BASE_IMAGE="python-dev-image" +TEST_IMAGE="python-dev-test-image" + +platforms="${1:-linux/amd64 linux/arm64}" + +# Build and test function that takes platform as parameter +build_and_test() { + local platform=$1 + local tag=$2 + + BASE_PLATFORM_IMAGE="$BASE_IMAGE-$platform:$tag" + TEST_PLATFORM_IMAGE="$TEST_IMAGE-$platform:$tag" + + echo "Building and testing for platform: $platform with tag: $tag" + + # Build the base image + docker build --load --platform "$platform" -t "$BASE_PLATFORM_IMAGE" -f Dockerfile . + + # Build the test image (use classic docker build for local-only workaround) + docker build --load --platform "$platform" -t "$TEST_PLATFORM_IMAGE" --build-arg BASE_IMAGE="$BASE_PLATFORM_IMAGE" --build-arg PLATFORM="$platform" -f tests/test-data/build-context/Dockerfile tests/test-data/build-context + + docker run --platform "$platform" --rm "$TEST_PLATFORM_IMAGE" uname -m + + if [ "$platform" == "linux/amd64" ]; then + container-structure-test test --platform "$platform" --image "$TEST_PLATFORM_IMAGE" --config tests/amd64.yaml + else + container-structure-test test --platform "$platform" --image "$TEST_PLATFORM_IMAGE" --config tests/arm64.yaml + fi + + # Run the tests + container-structure-test test --platform "$platform" --image "$TEST_PLATFORM_IMAGE" --config tests/specs.yaml + + # Clean up + docker rmi $TEST_IMAGE:"$tag" || true +} + +for platform in $platforms; do + # Build and test for each platform + build_and_test "$platform" "latest" +done diff --git a/tests/amd64.yaml b/tests/amd64.yaml new file mode 100644 index 0000000..3db2193 --- /dev/null +++ b/tests/amd64.yaml @@ -0,0 +1,7 @@ +schemaVersion: "2.0.0" + +commandTests: + - name: "is correct architecture" + command: "uname" + args: ["-m"] + expectedOutput: ["x86_64"] diff --git a/tests/arm64.yaml b/tests/arm64.yaml new file mode 100644 index 0000000..f1dc963 --- /dev/null +++ b/tests/arm64.yaml @@ -0,0 +1,7 @@ +schemaVersion: "2.0.0" + +commandTests: + - name: "is correct architecture" + command: "uname" + args: ["-m"] + expectedOutput: ["aarch64"] diff --git a/tests/specs.yaml b/tests/specs.yaml new file mode 100644 index 0000000..b673241 --- /dev/null +++ b/tests/specs.yaml @@ -0,0 +1,52 @@ +schemaVersion: "2.0.0" + +metadataTest: + exposedPorts: [] + +commandTests: + - name: "python is installed in the path" + command: "python" + args: ["--version"] + expectedOutput: ["Python 3.12"] + + - name: "uv is installed in path" + command: "uv" + args: ["--version"] + expectedOutput: ["uv"] + + - name: "shellcheck is installed in path" + command: "shellcheck" + args: ["--version"] + expectedOutput: ["shellcheck"] + + - name: "hadolint is installed in path" + command: "hadolint" + args: ["--version"] + + - name: "shfmt is installed in path" + command: "shfmt" + args: ["--version"] + + - name: "snyk is installed in path" + command: "snyk" + args: ["--version"] + + - name: "pulumi is installed in path" + command: "pulumi" + args: ["version"] + + - name: "reviewdog is installed in path" + command: "reviewdog" + args: ["--version"] + + - name: "actionlint is installed in path" + command: "actionlint" + args: ["--version"] + + - name: "watchman is installed in path" + command: "watchman" + args: ["--version"] + + - name: "ONBUILD was executed" + command: "ruff" + args: ["--version"] diff --git a/tests/test-data/build-context/.python-version b/tests/test-data/build-context/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/tests/test-data/build-context/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/tests/test-data/build-context/Dockerfile b/tests/test-data/build-context/Dockerfile new file mode 100644 index 0000000..6d9651c --- /dev/null +++ b/tests/test-data/build-context/Dockerfile @@ -0,0 +1,3 @@ +ARG BASE_IMAGE +ARG PLATFORM +FROM --platform=${PLATFORM} ${BASE_IMAGE} AS scratch diff --git a/tests/test-data/build-context/pyproject.toml b/tests/test-data/build-context/pyproject.toml new file mode 100644 index 0000000..5bd76fa --- /dev/null +++ b/tests/test-data/build-context/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "python-dev-image" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = "==3.12.*" +dependencies = [ + "ruff", +] diff --git a/tests/test-data/build-context/uv.lock b/tests/test-data/build-context/uv.lock new file mode 100644 index 0000000..a3a88b7 --- /dev/null +++ b/tests/test-data/build-context/uv.lock @@ -0,0 +1,39 @@ +version = 1 +revision = 1 +requires-python = "==3.12.*" + +[[package]] +name = "python-dev-image" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "ruff" }] + +[[package]] +name = "ruff" +version = "0.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/11/bcef6784c7e5d200b8a1f5c2ddf53e5da0efec37e6e5a44d163fb97e04ba/ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79", size = 4010053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/1f/8848b625100ebcc8740c8bac5b5dd8ba97dd4ee210970e98832092c1635b/ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1", size = 10248105 }, + { url = "https://files.pythonhosted.org/packages/e0/47/c44036e70c6cc11e6ee24399c2a1e1f1e99be5152bd7dff0190e4b325b76/ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de", size = 11001494 }, + { url = "https://files.pythonhosted.org/packages/ed/5b/170444061650202d84d316e8f112de02d092bff71fafe060d3542f5bc5df/ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a", size = 10352151 }, + { url = "https://files.pythonhosted.org/packages/ff/91/f02839fb3787c678e112c8865f2c3e87cfe1744dcc96ff9fc56cfb97dda2/ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193", size = 10541951 }, + { url = "https://files.pythonhosted.org/packages/9e/f3/c09933306096ff7a08abede3cc2534d6fcf5529ccd26504c16bf363989b5/ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e", size = 10079195 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/a87f8933fccbc0d8c653cfbf44bedda69c9582ba09210a309c066794e2ee/ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308", size = 11698918 }, + { url = "https://files.pythonhosted.org/packages/52/7d/8eac0bd083ea8a0b55b7e4628428203441ca68cd55e0b67c135a4bc6e309/ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55", size = 12319426 }, + { url = "https://files.pythonhosted.org/packages/c2/dc/d0c17d875662d0c86fadcf4ca014ab2001f867621b793d5d7eef01b9dcce/ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc", size = 11791012 }, + { url = "https://files.pythonhosted.org/packages/f9/f3/81a1aea17f1065449a72509fc7ccc3659cf93148b136ff2a8291c4bc3ef1/ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2", size = 13949947 }, + { url = "https://files.pythonhosted.org/packages/61/9f/a3e34de425a668284e7024ee6fd41f452f6fa9d817f1f3495b46e5e3a407/ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6", size = 11471753 }, + { url = "https://files.pythonhosted.org/packages/df/c5/4a57a86d12542c0f6e2744f262257b2aa5a3783098ec14e40f3e4b3a354a/ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2", size = 10417121 }, + { url = "https://files.pythonhosted.org/packages/58/3f/a3b4346dff07ef5b862e2ba06d98fcbf71f66f04cf01d375e871382b5e4b/ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03", size = 10073829 }, + { url = "https://files.pythonhosted.org/packages/93/cc/7ed02e0b86a649216b845b3ac66ed55d8aa86f5898c5f1691797f408fcb9/ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b", size = 11076108 }, + { url = "https://files.pythonhosted.org/packages/39/5e/5b09840fef0eff1a6fa1dea6296c07d09c17cb6fb94ed5593aa591b50460/ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9", size = 11512366 }, + { url = "https://files.pythonhosted.org/packages/6f/4c/1cd5a84a412d3626335ae69f5f9de2bb554eea0faf46deb1f0cb48534042/ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287", size = 10485900 }, + { url = "https://files.pythonhosted.org/packages/42/46/8997872bc44d43df986491c18d4418f1caff03bc47b7f381261d62c23442/ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e", size = 11558592 }, + { url = "https://files.pythonhosted.org/packages/d7/6a/65fecd51a9ca19e1477c3879a7fda24f8904174d1275b419422ac00f6eee/ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79", size = 10682766 }, +]