diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..5987a3f --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,56 @@ +name: Docker +on: + release: + types: [published] + workflow_dispatch: + inputs: + build_tag: + description: "Build tag suffix (appended after the cli version)" + default: "0" + required: true + +permissions: + contents: read + +jobs: + build-push: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v5 + with: + python-version: "3.14" + - name: Verify SENTIEON_VERSION repo variable + env: + SENTIEON_VERSION: ${{ vars.SENTIEON_VERSION }} + run: | + if [ -z "$SENTIEON_VERSION" ]; then + echo "::error::Repository variable SENTIEON_VERSION is not set." >&2 + exit 1 + fi + echo "Building against Sentieon $SENTIEON_VERSION" + - name: Resolve build metadata + id: meta + run: | + version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])') + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64 + push: true + tags: | + sentieon/sentieon-cli:${{ steps.meta.outputs.version }}-${{ inputs.build_tag || '0' }} + sentieon/sentieon-cli:latest + build-args: | + SENTIEON_VERSION=${{ vars.SENTIEON_VERSION }} + VCS_REF=${{ github.sha }} + BUILD_DATE=${{ steps.meta.outputs.build_date }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b02b53..6a70ce0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,29 @@ jobs: sentieon-cli -h - name: build run: | - poetry build -f sdist - gh release upload ${{github.event.release.tag_name}} dist/*.tar.gz + poetry build + gh release upload ${{github.event.release.tag_name}} dist/*.tar.gz dist/*.whl env: GITHUB_TOKEN: ${{ github.TOKEN }} + - name: Upload distribution artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/* + + publish-pypi: + needs: build-release + runs-on: ubuntu-22.04 + environment: + name: pypi + url: https://pypi.org/p/sentieon_cli + permissions: + id-token: write + steps: + - name: Download distribution artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/Dockerfile b/Dockerfile index 7cfe398..800b254 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,16 @@ ARG VG_SHA256=d5752977237e801d971f0d044f43fde4f3005da35f0451b4f452d1c1c9b8414b ARG KMC_VERSION=3.2.4 ARG KMC_RELEASE_TAG=v3.2.4-pipe2 ARG KMC_SHA256=d8bdf8edcd0577dba32e86e8f194b2eb04eb168c75a3e6d46721d5bef515ac96 +ARG EXPANSIONHUNTER_VERSION=5.0.0 +ARG EXPANSIONHUNTER_SHA256=ebf3ec0ace6e6e3bbce12c26463da5d9f8e16374eff1ad10f0f1a9123050fa86 +# T1K and segdup-caller are built from git; pin the tag and verify the +# resolved commit SHA after checkout. +ARG T1K_VERSION=1.0.9 +ARG T1K_GIT_TAG=v1.0.9 +ARG T1K_COMMIT=9376b555c1d8d2f8ca357c2656f49f450462dbc3 +ARG SEGDUP_CALLER_VERSION=0.5.1 +ARG SEGDUP_CALLER_GIT_TAG=v0.5.1 +ARG SEGDUP_CALLER_COMMIT=0406ea78b7ff53d7a169b9aa945d7fcc7257ff12 # Pinned Poetry toolchain; should match the version used to generate poetry.lock. ARG POETRY_VERSION=2.3.4 ARG POETRY_PLUGIN_EXPORT_VERSION=1.9.0 @@ -27,7 +37,7 @@ RUN test -n "$SENTIEON_VERSION" # Install all build dependencies for the downloader stage in one pass. RUN apt-get update && apt-get install -y --no-install-recommends \ - curl ca-certificates bzip2 autoconf automake libtool make gcc perl \ + curl ca-certificates bzip2 autoconf automake libtool make gcc g++ perl \ nasm git python3 python3-venv zlib1g-dev libbz2-dev liblzma-dev \ libcurl4-gnutls-dev libssl-dev libncurses5-dev libdeflate-dev \ libperl-dev libgsl0-dev && \ @@ -94,6 +104,31 @@ RUN mkdir -p /opt/kmc-${KMC_VERSION} && \ /usr/local/bin/kmc_tools-${KMC_VERSION} \ /usr/local/bin/kmc_dump-${KMC_VERSION} +# Install ExpansionHunter +RUN mkdir -p /tmp/eh && \ + curl -fL -o /tmp/eh.tar.gz "https://github.com/Illumina/ExpansionHunter/releases/download/v${EXPANSIONHUNTER_VERSION}/ExpansionHunter-v${EXPANSIONHUNTER_VERSION}-linux_x86_64.tar.gz" && \ + echo "${EXPANSIONHUNTER_SHA256} /tmp/eh.tar.gz" | sha256sum -c - && \ + tar -C /tmp/eh --strip-components=1 -zxf /tmp/eh.tar.gz && \ + cp /tmp/eh/bin/ExpansionHunter /usr/local/bin/ExpansionHunter-${EXPANSIONHUNTER_VERSION} && \ + chmod +x /usr/local/bin/ExpansionHunter-${EXPANSIONHUNTER_VERSION} && \ + rm -rf /tmp/eh /tmp/eh.tar.gz + +# Build T1K from source. +RUN git clone --branch ${T1K_GIT_TAG} https://github.com/mourisl/T1K.git /tmp/t1k-src && \ + cd /tmp/t1k-src && \ + test "$(git rev-parse HEAD)" = "${T1K_COMMIT}" && \ + make -j"$(nproc)" && \ + mkdir -p /opt/t1k-${T1K_VERSION} && \ + cp run-t1k bam-extractor fastq-extractor genotyper analyzer \ + /opt/t1k-${T1K_VERSION}/ && \ + rm -rf /tmp/t1k-src + +# Clone segdup-caller. The actual `pip install` happens in the +# python-builder stage so the venv lives in a clean image. +RUN git clone --branch ${SEGDUP_CALLER_GIT_TAG} https://github.com/Sentieon/segdup-caller.git /opt/segdup-caller-src && \ + cd /opt/segdup-caller-src && \ + test "$(git rev-parse HEAD)" = "${SEGDUP_CALLER_COMMIT}" + # Download the Sentieon software RUN mkdir -p /opt/sentieon/ && \ curl -fL "https://s3.amazonaws.com/sentieon-release/software/sentieon-genomics-${SENTIEON_VERSION}.tar.gz" | \ @@ -124,12 +159,16 @@ FROM debian:13.4-slim AS python-builder ARG SENTIEON_VERSION COPY --from=downloader /opt/sentieon-cli/dist /opt/sentieon-cli/dist +COPY --from=downloader /opt/segdup-caller-src /opt/segdup-caller-src +# Build the sentieon-cli venv and a separate venv for segdup-caller. +# segdup-caller pulls in whatshap/pysam/scipy/pandas which would otherwise +# bloat (and risk version-conflicting with) the sentieon-cli env. # Remove the kaleido and pyarrow packages to reduce image size. RUN apt-get update && \ apt-get install -y --no-install-recommends \ python3 python3-venv python3-dev \ - gcc g++ make zlib1g-dev && \ + gcc g++ make zlib1g-dev libbz2-dev liblzma-dev libcurl4-gnutls-dev && \ rm -rf /var/lib/apt/lists/* && \ python3 -m venv /opt/sentieon-cli-venv && \ /opt/sentieon-cli-venv/bin/pip install --no-cache-dir \ @@ -138,7 +177,12 @@ RUN apt-get update && \ /opt/sentieon-cli/dist/*.whl && \ /opt/sentieon-cli-venv/bin/pip uninstall -y kaleido pyarrow && \ /opt/sentieon-cli-venv/bin/pip uninstall -y pip setuptools wheel && \ - find /opt/sentieon-cli-venv -depth -type d -name __pycache__ -exec rm -rf {} + + python3 -m venv /opt/segdup-caller-venv && \ + /opt/segdup-caller-venv/bin/pip install --no-cache-dir \ + /opt/segdup-caller-src && \ + /opt/segdup-caller-venv/bin/pip uninstall -y pip setuptools wheel && \ + find /opt/sentieon-cli-venv /opt/segdup-caller-venv \ + -depth -type d -name __pycache__ -exec rm -rf {} + # Build the container FROM debian:13.4-slim @@ -147,6 +191,8 @@ ARG BEDTOOLS_VERSION=2.30.0 ARG MOSDEPTH_VERSION=0.3.9 ARG VG_VERSION=1.73.0 ARG KMC_VERSION=3.2.4 +ARG EXPANSIONHUNTER_VERSION=5.0.0 +ARG T1K_VERSION=1.0.9 ARG VCS_REF ARG BUILD_DATE @@ -175,10 +221,18 @@ COPY --from=downloader /usr/local/bin/vg-${VG_VERSION} /usr/local/bin/vg-${VG_VE COPY --from=downloader /usr/local/bin/kmc-${KMC_VERSION} /usr/local/bin/kmc-${KMC_VERSION} COPY --from=downloader /usr/local/bin/kmc_tools-${KMC_VERSION} /usr/local/bin/kmc_tools-${KMC_VERSION} COPY --from=downloader /usr/local/bin/kmc_dump-${KMC_VERSION} /usr/local/bin/kmc_dump-${KMC_VERSION} +COPY --from=downloader /usr/local/bin/ExpansionHunter-${EXPANSIONHUNTER_VERSION} /usr/local/bin/ExpansionHunter-${EXPANSIONHUNTER_VERSION} +COPY --from=downloader /opt/t1k-${T1K_VERSION} /opt/t1k-${T1K_VERSION} COPY --from=downloader /opt/sentieon-cli/dist /opt/sentieon-cli/dist # Create symlinks for libisal (the .so lives on the default linker path in # /usr/lib) and for the unversioned names of the third-party tools. +# `run-t1k` resolves its sibling binaries via abs_path($0), so symlinking +# it from /usr/local/bin keeps the binaries discoverable in /opt/t1k-*/. +# `ExpansionHunter` goes through a wrapper that strips LD_PRELOAD: the +# pre-built EH binary crashes with `std::bad_cast` while parsing its JSON +# variant catalog when the global jemalloc preload (set further down) is +# active. RUN cd /usr/lib && \ ln -s libisal.so.2.0.30 libisal.so.2 && \ ln -s libisal.so.2 libisal.so && \ @@ -188,13 +242,18 @@ RUN cd /usr/lib && \ ln -s vg-${VG_VERSION} vg && \ ln -s kmc-${KMC_VERSION} kmc && \ ln -s kmc_tools-${KMC_VERSION} kmc_tools && \ - ln -s kmc_dump-${KMC_VERSION} kmc_dump + ln -s kmc_dump-${KMC_VERSION} kmc_dump && \ + ln -s /opt/t1k-${T1K_VERSION}/run-t1k run-t1k && \ + printf '#!/bin/sh\nexec env -u LD_PRELOAD /usr/local/bin/ExpansionHunter-%s "$@"\n' \ + "${EXPANSIONHUNTER_VERSION}" > ExpansionHunter && \ + chmod +x ExpansionHunter # Install runtime dependencies. RUN apt-get update && \ apt-get install -y --no-install-recommends \ libjemalloc2 procps libdeflate0 libbz2-1.0 liblzma5 \ libcurl3-gnutls libssl3 libperl5.40 libgsl28 libncurses6 \ + libstdc++6 perl \ curl python3 && \ rm -rf /var/lib/apt/lists/* @@ -205,11 +264,15 @@ ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 # A default jemalloc configuration that should work well for most use-cases, see http://jemalloc.net/jemalloc.3.html ENV MALLOC_CONF=metadata_thp:auto,background_thread:true,dirty_decay_ms:30000,muzzy_decay_ms:30000 -# Copy the pre-built venv from the python-builder stage. No compilers -# are installed in this stage. +# Copy the pre-built venvs from the python-builder stage. No compilers +# are installed in this stage. The segdup-caller venv stays isolated; +# only its CLI entry point is exposed on PATH. COPY --from=python-builder /opt/sentieon-cli-venv /opt/sentieon-cli-venv +COPY --from=python-builder /opt/segdup-caller-venv /opt/segdup-caller-venv ENV VIRTUAL_ENV=/opt/sentieon-cli-venv ENV PATH=/opt/sentieon-cli-venv/bin:$PATH +# Append (not prepend) the segdup-caller venv +ENV PATH=$PATH:/opt/segdup-caller-venv/bin # Create a non-root user for running the pipelines RUN useradd --create-home --uid 1001 --shell /bin/bash sentieon @@ -226,4 +289,11 @@ RUN sentieon driver --help && \ mosdepth -h && \ vg version && \ kmc --help && \ + ExpansionHunter --help && \ + perl -c "$(command -v run-t1k)" && \ + segdup-caller --version && \ + python -c "import sys; from packaging.version import Version; \ +from sentieon_cli.sentieon_pangenome import SEGDUP_MIN_VERSION; \ +from sentieon_cli.util import check_version; \ +sys.exit(0 if all(check_version(c, v) for c, v in SEGDUP_MIN_VERSION.items()) else 1)" && \ sentieon-cli -h diff --git a/poetry.lock b/poetry.lock index 0117c37..9656819 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -1109,86 +1109,86 @@ test-extras = ["pytest-mpl", "pytest-randomly"] [[package]] name = "numpy" -version = "2.4.3" +version = "2.4.4" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.11" -groups = ["main", "container"] -files = [ - {file = "numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb"}, - {file = "numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147"}, - {file = "numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920"}, - {file = "numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9"}, - {file = "numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470"}, - {file = "numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71"}, - {file = "numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15"}, - {file = "numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52"}, - {file = "numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd"}, - {file = "numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec"}, - {file = "numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67"}, - {file = "numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef"}, - {file = "numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e"}, - {file = "numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4"}, - {file = "numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18"}, - {file = "numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5"}, - {file = "numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97"}, - {file = "numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c"}, - {file = "numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc"}, - {file = "numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9"}, - {file = "numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5"}, - {file = "numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e"}, - {file = "numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3"}, - {file = "numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9"}, - {file = "numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee"}, - {file = "numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f"}, - {file = "numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f"}, - {file = "numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc"}, - {file = "numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476"}, - {file = "numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92"}, - {file = "numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687"}, - {file = "numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd"}, - {file = "numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d"}, - {file = "numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875"}, - {file = "numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070"}, - {file = "numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73"}, - {file = "numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368"}, - {file = "numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22"}, - {file = "numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a"}, - {file = "numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349"}, - {file = "numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c"}, - {file = "numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26"}, - {file = "numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02"}, - {file = "numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4"}, - {file = "numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168"}, - {file = "numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b"}, - {file = "numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950"}, - {file = "numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd"}, - {file = "numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24"}, - {file = "numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0"}, - {file = "numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0"}, - {file = "numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a"}, - {file = "numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc"}, - {file = "numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7"}, - {file = "numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657"}, - {file = "numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7"}, - {file = "numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093"}, - {file = "numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a"}, - {file = "numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611"}, - {file = "numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720"}, - {file = "numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5"}, - {file = "numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0"}, - {file = "numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b"}, - {file = "numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e"}, - {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028"}, - {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8"}, - {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152"}, - {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395"}, - {file = "numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79"}, - {file = "numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857"}, - {file = "numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5"}, - {file = "numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd"}, +groups = ["container"] +markers = "python_full_version != \"3.14.1\"" +files = [ + {file = "numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db"}, + {file = "numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0"}, + {file = "numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015"}, + {file = "numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40"}, + {file = "numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d"}, + {file = "numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502"}, + {file = "numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd"}, + {file = "numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5"}, + {file = "numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e"}, + {file = "numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e"}, + {file = "numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e"}, + {file = "numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b"}, + {file = "numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e"}, + {file = "numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842"}, + {file = "numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8"}, + {file = "numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121"}, + {file = "numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e"}, + {file = "numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44"}, + {file = "numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d"}, + {file = "numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827"}, + {file = "numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a"}, + {file = "numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec"}, + {file = "numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50"}, + {file = "numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115"}, + {file = "numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af"}, + {file = "numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c"}, + {file = "numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103"}, + {file = "numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83"}, + {file = "numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed"}, + {file = "numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959"}, + {file = "numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed"}, + {file = "numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf"}, + {file = "numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d"}, + {file = "numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5"}, + {file = "numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7"}, + {file = "numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93"}, + {file = "numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e"}, + {file = "numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40"}, + {file = "numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e"}, + {file = "numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392"}, + {file = "numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008"}, + {file = "numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8"}, + {file = "numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233"}, + {file = "numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0"}, + {file = "numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a"}, + {file = "numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a"}, + {file = "numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b"}, + {file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a"}, + {file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d"}, + {file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252"}, + {file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f"}, + {file = "numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc"}, + {file = "numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74"}, + {file = "numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb"}, + {file = "numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e"}, + {file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113"}, + {file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d"}, + {file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d"}, + {file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f"}, + {file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0"}, + {file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150"}, + {file = "numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871"}, + {file = "numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e"}, + {file = "numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f"}, + {file = "numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119"}, + {file = "numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0"}, ] -markers = {container = "python_full_version != \"3.14.1\""} [[package]] name = "packaging" @@ -1916,95 +1916,95 @@ files = [ [[package]] name = "rapidfuzz" -version = "3.14.3" +version = "3.14.5" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "rapidfuzz-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9fcd4d751a4fffa17aed1dde41647923c72c74af02459ad1222e3b0022da3a1"}, - {file = "rapidfuzz-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ad73afb688b36864a8d9b7344a9cf6da186c471e5790cbf541a635ee0f457f2"}, - {file = "rapidfuzz-3.14.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5fb2d978a601820d2cfd111e2c221a9a7bfdf84b41a3ccbb96ceef29f2f1ac7"}, - {file = "rapidfuzz-3.14.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d83b8b712fa37e06d59f29a4b49e2e9e8635e908fbc21552fe4d1163db9d2a1"}, - {file = "rapidfuzz-3.14.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:dc8c07801df5206b81ed6bd6c35cb520cf9b6c64b9b0d19d699f8633dc942897"}, - {file = "rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c71ce6d4231e5ef2e33caa952bfe671cb9fd42e2afb11952df9fad41d5c821f9"}, - {file = "rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0e38828d1381a0cceb8a4831212b2f673d46f5129a1897b0451c883eaf4a1747"}, - {file = "rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da2a007434323904719158e50f3076a4dadb176ce43df28ed14610c773cc9825"}, - {file = "rapidfuzz-3.14.3-cp310-cp310-win32.whl", hash = "sha256:fce3152f94afcfd12f3dd8cf51e48fa606e3cb56719bccebe3b401f43d0714f9"}, - {file = "rapidfuzz-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:37d3c653af15cd88592633e942f5407cb4c64184efab163c40fcebad05f25141"}, - {file = "rapidfuzz-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:cc594bbcd3c62f647dfac66800f307beaee56b22aaba1c005e9c4c40ed733923"}, - {file = "rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d"}, - {file = "rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3"}, - {file = "rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850"}, - {file = "rapidfuzz-3.14.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe8df315ab4e6db4e1be72c5170f8e66021acde22cd2f9d04d2058a9fd8162e"}, - {file = "rapidfuzz-3.14.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:769f31c60cd79420188fcdb3c823227fc4a6deb35cafec9d14045c7f6743acae"}, - {file = "rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54fa03062124e73086dae66a3451c553c1e20a39c077fd704dc7154092c34c63"}, - {file = "rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:834d1e818005ed0d4ae38f6b87b86fad9b0a74085467ece0727d20e15077c094"}, - {file = "rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:948b00e8476a91f510dd1ec07272efc7d78c275d83b630455559671d4e33b678"}, - {file = "rapidfuzz-3.14.3-cp311-cp311-win32.whl", hash = "sha256:43d0305c36f504232f18ea04e55f2059bb89f169d3119c4ea96a0e15b59e2a91"}, - {file = "rapidfuzz-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:ef6bf930b947bd0735c550683939a032090f1d688dfd8861d6b45307b96fd5c5"}, - {file = "rapidfuzz-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:f3eb0ff3b75d6fdccd40b55e7414bb859a1cda77c52762c9c82b85569f5088e7"}, - {file = "rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226"}, - {file = "rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb"}, - {file = "rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941"}, - {file = "rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382"}, - {file = "rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43"}, - {file = "rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db"}, - {file = "rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed"}, - {file = "rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc"}, - {file = "rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a"}, - {file = "rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329"}, - {file = "rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f"}, - {file = "rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae"}, - {file = "rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff"}, - {file = "rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457"}, - {file = "rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c"}, - {file = "rapidfuzz-3.14.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:e195a77d06c03c98b3fc06b8a28576ba824392ce40de8c708f96ce04849a052e"}, - {file = "rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b7ef2f4b8583a744338a18f12c69693c194fb6777c0e9ada98cd4d9e8f09d10"}, - {file = "rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a2135b138bcdcb4c3742d417f215ac2d8c2b87bde15b0feede231ae95f09ec41"}, - {file = "rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33a325ed0e8e1aa20c3e75f8ab057a7b248fdea7843c2a19ade0008906c14af0"}, - {file = "rapidfuzz-3.14.3-cp313-cp313-win32.whl", hash = "sha256:8383b6d0d92f6cd008f3c9216535be215a064b2cc890398a678b56e6d280cb63"}, - {file = "rapidfuzz-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:e6b5e3036976f0fde888687d91be86d81f9ac5f7b02e218913c38285b756be6c"}, - {file = "rapidfuzz-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:7ba009977601d8b0828bfac9a110b195b3e4e79b350dcfa48c11269a9f1918a0"}, - {file = "rapidfuzz-3.14.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0a28add871425c2fe94358c6300bbeb0bc2ed828ca003420ac6825408f5a424"}, - {file = "rapidfuzz-3.14.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010e12e2411a4854b0434f920e72b717c43f8ec48d57e7affe5c42ecfa05dd0e"}, - {file = "rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cfc3d57abd83c734d1714ec39c88a34dd69c85474918ebc21296f1e61eb5ca8"}, - {file = "rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89acb8cbb52904f763e5ac238083b9fc193bed8d1f03c80568b20e4cef43a519"}, - {file = "rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_31_armv7l.whl", hash = "sha256:7d9af908c2f371bfb9c985bd134e295038e3031e666e4b2ade1e7cb7f5af2f1a"}, - {file = "rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1f1925619627f8798f8c3a391d81071336942e5fe8467bc3c567f982e7ce2897"}, - {file = "rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:152555187360978119e98ce3e8263d70dd0c40c7541193fc302e9b7125cf8f58"}, - {file = "rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52619d25a09546b8db078981ca88939d72caa6b8701edd8b22e16482a38e799f"}, - {file = "rapidfuzz-3.14.3-cp313-cp313t-win32.whl", hash = "sha256:489ce98a895c98cad284f0a47960c3e264c724cb4cfd47a1430fa091c0c25204"}, - {file = "rapidfuzz-3.14.3-cp313-cp313t-win_amd64.whl", hash = "sha256:656e52b054d5b5c2524169240e50cfa080b04b1c613c5f90a2465e84888d6f15"}, - {file = "rapidfuzz-3.14.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c7e40c0a0af02ad6e57e89f62bef8604f55a04ecae90b0ceeda591bbf5923317"}, - {file = "rapidfuzz-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:442125473b247227d3f2de807a11da6c08ccf536572d1be943f8e262bae7e4ea"}, - {file = "rapidfuzz-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ec0c8c0c3d4f97ced46b2e191e883f8c82dbbf6d5ebc1842366d7eff13cd5a6"}, - {file = "rapidfuzz-3.14.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dc37bc20272f388b8c3a4eba4febc6e77e50a8f450c472def4751e7678f55e4"}, - {file = "rapidfuzz-3.14.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee362e7e79bae940a5e2b3f6d09c6554db6a4e301cc68343886c08be99844f1"}, - {file = "rapidfuzz-3.14.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:4b39921df948388a863f0e267edf2c36302983459b021ab928d4b801cbe6a421"}, - {file = "rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beda6aa9bc44d1d81242e7b291b446be352d3451f8217fcb068fc2933927d53b"}, - {file = "rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6a014ba09657abfcfeed64b7d09407acb29af436d7fc075b23a298a7e4a6b41c"}, - {file = "rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32eeafa3abce138bb725550c0e228fc7eaeec7059aa8093d9cbbec2b58c2371a"}, - {file = "rapidfuzz-3.14.3-cp314-cp314-win32.whl", hash = "sha256:adb44d996fc610c7da8c5048775b21db60dd63b1548f078e95858c05c86876a3"}, - {file = "rapidfuzz-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:f3d15d8527e2b293e38ce6e437631af0708df29eafd7c9fc48210854c94472f9"}, - {file = "rapidfuzz-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:576e4b9012a67e0bf54fccb69a7b6c94d4e86a9540a62f1a5144977359133583"}, - {file = "rapidfuzz-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cec3c0da88562727dd5a5a364bd9efeb535400ff0bfb1443156dd139a1dd7b50"}, - {file = "rapidfuzz-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d1fa009f8b1100e4880868137e7bf0501422898f7674f2adcd85d5a67f041296"}, - {file = "rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b86daa7419b5e8b180690efd1fdbac43ff19230803282521c5b5a9c83977655"}, - {file = "rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7bd1816db05d6c5ffb3a4df0a2b7b56fb8c81ef584d08e37058afa217da91b1"}, - {file = "rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:33da4bbaf44e9755b0ce192597f3bde7372fe2e381ab305f41b707a95ac57aa7"}, - {file = "rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3fecce764cf5a991ee2195a844196da840aba72029b2612f95ac68a8b74946bf"}, - {file = "rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ecd7453e02cf072258c3a6b8e930230d789d5d46cc849503729f9ce475d0e785"}, - {file = "rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea188aa00e9bcae8c8411f006a5f2f06c4607a02f24eab0d8dc58566aa911f35"}, - {file = "rapidfuzz-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:7ccbf68100c170e9a0581accbe9291850936711548c6688ce3bfb897b8c589ad"}, - {file = "rapidfuzz-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9ec02e62ae765a318d6de38df609c57fc6dacc65c0ed1fd489036834fd8a620c"}, - {file = "rapidfuzz-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e805e52322ae29aa945baf7168b6c898120fbc16d2b8f940b658a5e9e3999253"}, - {file = "rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7cf174b52cb3ef5d49e45d0a1133b7e7d0ecf770ed01f97ae9962c5c91d97d23"}, - {file = "rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:442cba39957a008dfc5bdef21a9c3f4379e30ffb4e41b8555dbaf4887eca9300"}, - {file = "rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1faa0f8f76ba75fd7b142c984947c280ef6558b5067af2ae9b8729b0a0f99ede"}, - {file = "rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e6eefec45625c634926a9fd46c9e4f31118ac8f3156fff9494422cee45207e6"}, - {file = "rapidfuzz-3.14.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56fefb4382bb12250f164250240b9dd7772e41c5c8ae976fd598a32292449cc5"}, - {file = "rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f"}, + {file = "rapidfuzz-3.14.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:071d96b957a33b9296b9284b6350a0fb6d030b154a04efd7c15e56b98b79a517"}, + {file = "rapidfuzz-3.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667f40fe9c81ad129b198d236881b00dd9e8314d9cc72d03c3e16bdfe5879051"}, + {file = "rapidfuzz-3.14.5-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9fff308486bbd2c8c24f25e8e152c7594d3fe8db265a2d6a1ce24d58671127f"}, + {file = "rapidfuzz-3.14.5-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dfa552338f51aec280f17b02d28bace1e162d1a84ccd80e3339a57f98aedb56b"}, + {file = "rapidfuzz-3.14.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:068b3e965ca9d9ee4debe40001ae7c3938ba646308afd33cf0c66618147db65c"}, + {file = "rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88b7d31ff1cc5e9bc0e4406e6b1fa00b6d37163d50bb58091e9b976ff1129faa"}, + {file = "rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eacb434410b8d9ca99a8d42352ef085cf423e3c76c1f0b86be2fcba3bff2952c"}, + {file = "rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:649712823f3abcdc48427147a5384fac15623ba435d0013959b52e6462521397"}, + {file = "rapidfuzz-3.14.5-cp310-cp310-win32.whl", hash = "sha256:13cb79c23ef5516e4c4e3830877be8b19aa75203636be1163d690d37803f6504"}, + {file = "rapidfuzz-3.14.5-cp310-cp310-win_amd64.whl", hash = "sha256:f2073495a7f9b75e57e600747ac09510d67683fd64d3228e009740b7ef88f9fe"}, + {file = "rapidfuzz-3.14.5-cp310-cp310-win_arm64.whl", hash = "sha256:8166efddea49fdbc61185559f47593239e4794fd7c9044dd5a789d1a90af852d"}, + {file = "rapidfuzz-3.14.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e251126d48615e1f02b4a178f2cd0cd4f0332b8a019c01a2e10480f7552554b4"}, + {file = "rapidfuzz-3.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ab449c9abd0d4e1f8145dce0798a4c822a1a1933d613c764a641bea88b8bdab"}, + {file = "rapidfuzz-3.14.5-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb2829fedd672dd7107267189dabe2bbe07972801d636014417c6861eb89e358"}, + {file = "rapidfuzz-3.14.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d50e5861872935fece391351cbb5ba21d1bced277cf5e1143d207a0a35f1925"}, + {file = "rapidfuzz-3.14.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:7092a216728f80c960bd6b3807275d1ee318b168986bd5dc523349581d4890b8"}, + {file = "rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9669753caef7fdc6529f6adcc5883ed98d65976445d9322e7dbdb6b697feee13"}, + {file = "rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:823b1b9d9230809d8edcc18872770764bfe8ef4357995e16744047c8ccf0e489"}, + {file = "rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f0b2af76b7e7060c09e1a0dfa9410eb19369cbe6164509bff2ef94094b54d2b6"}, + {file = "rapidfuzz-3.14.5-cp311-cp311-win32.whl", hash = "sha256:c5801a89604c65ab4cc9e91b23bc4076d0ca80efd8c976fb63843d7879a85d7f"}, + {file = "rapidfuzz-3.14.5-cp311-cp311-win_amd64.whl", hash = "sha256:d7ca16637c0ede8243f84074044bd0b2335a0341421f8227c85756de2d18c819"}, + {file = "rapidfuzz-3.14.5-cp311-cp311-win_arm64.whl", hash = "sha256:8c90cdf8516d9057e502aa6003cea71cf5ec27cc44699ca52412b502a04761bb"}, + {file = "rapidfuzz-3.14.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d3378f471ef440473a396ce2f8e97ee12f89a78b495540e0a5617bbfe895638"}, + {file = "rapidfuzz-3.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e910eebca9fd0eba245c0555e764597e8a0cccb673a92da2dc2397050725f48"}, + {file = "rapidfuzz-3.14.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01550fe5f60fd176aa66b7611289d46dc4aa4b1b904874c7b6d1d54e581c5ec1"}, + {file = "rapidfuzz-3.14.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48bee0b91bebfaec41e1081e351000659ab7570cc4598d617aa04d5bf827f9e6"}, + {file = "rapidfuzz-3.14.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:7e580cb04ad849ae9b786fa21383c6b994b6e6c1444ad1cb9f22392759d72741"}, + {file = "rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:09d6c9ba091854f07817055d795d604179c12a8f308ba4c7d56f3719dfea1646"}, + {file = "rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1e989f86113be66574113b9c7bdf4793f3f863d248e47d911b355e05ca6b6b10"}, + {file = "rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ebd1a18e2e47bc0b292a07e6ed9c3642f8aaa672d12253885f599b50807a4f9"}, + {file = "rapidfuzz-3.14.5-cp312-cp312-win32.whl", hash = "sha256:9981d38a703b86f0e315a3cd229fd1906fe1d91c989ed121fb975b3c849f89f5"}, + {file = "rapidfuzz-3.14.5-cp312-cp312-win_amd64.whl", hash = "sha256:d8375e3da319593389727c3187ccaf3e0e84199accc530866b8e0f2b79af05e9"}, + {file = "rapidfuzz-3.14.5-cp312-cp312-win_arm64.whl", hash = "sha256:478b59bb018a6780d73f33e38d0b3ec5e968a6c1ed42876b993dd456b7aa20e8"}, + {file = "rapidfuzz-3.14.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd8fd343bf8492a1e60bcb6dc99f90f74f65d98d8241a6b3e1fed225b76ecd6"}, + {file = "rapidfuzz-3.14.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6737b35d5af7479c5bf9710f7b17edd9d2c43128d974d25fb4ea653e42c64609"}, + {file = "rapidfuzz-3.14.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b002c7994cc9f2bc9d9856f0fbaee6e8072c983873846c92f25cefba5b2a925f"}, + {file = "rapidfuzz-3.14.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17a34330cd2a538c1ce5d400b61ba358c5b72c654b928ff87b362e88f8b864c7"}, + {file = "rapidfuzz-3.14.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:95d937e74c1a7a1287dfb03b62a827be08ede10a155cf1af73bbf47f2b73ee6e"}, + {file = "rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:46b92a9970dcc34f0096901c792644094cab49554ac3547f35e3aebbdf0a3610"}, + {file = "rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e012177c8e8a8a0754ae0d6027d63042aa5ff036d9f40f07cb3466a6082e21b8"}, + {file = "rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ae6f53f99c9a0eca7a0afc5b4e45fc73bc1dd4ac74c00509031d76df80ed98"}, + {file = "rapidfuzz-3.14.5-cp313-cp313-win32.whl", hash = "sha256:4a60f0057231188e3bd30216f7b4e0f279b11fa4ec818bb6c1d9f014d1562fbc"}, + {file = "rapidfuzz-3.14.5-cp313-cp313-win_amd64.whl", hash = "sha256:11bfc2ed8fbe4ab86bd516fadefab126f90e6dcadffa761739fcb304707dfd35"}, + {file = "rapidfuzz-3.14.5-cp313-cp313-win_arm64.whl", hash = "sha256:b486b5218808f6f4dc471b114b1054e63553db69705c97da0271f47bd706aedd"}, + {file = "rapidfuzz-3.14.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39ef8658aaf67d51667e7bdaf7096f432333377d8302ac43c70b5df8a4cf89b8"}, + {file = "rapidfuzz-3.14.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ad37a0be705b544af6296da8edddc260d10a8ae5462530fc9991f66498bb1f9"}, + {file = "rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d45e06f60729e07d9b20c205f7e5cff90b6ef2584e852eecf46e045aea69627d"}, + {file = "rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e52da10236aa6212de71b9e170bace65b64b129c0dea7fc243d6c9ce976f5074"}, + {file = "rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:440d30faaf682ca496170a7f0cc5453ec942e3e079f0fd802c9a7f938dfb50a3"}, + {file = "rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:56227a61fd3d17b0cd9793132431f3a3d07c8654be96794ba9f89fe0fc8b2d09"}, + {file = "rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:2e83cd2e25bb4edd97b689d9979d9c3acccdaaf26ceac08212ceece202febcfa"}, + {file = "rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:af3b859726cd3374287e405e14b9634563c078c5531a4f62375508addebddad1"}, + {file = "rapidfuzz-3.14.5-cp313-cp313t-win32.whl", hash = "sha256:8ce1d850b3c0178440efde9e884d98421b5e87ff925f364d6d79e23910d7593f"}, + {file = "rapidfuzz-3.14.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c84af70bcf34e99aee894e46a0f1ac77f17d0ef828179c387407642e2466d28a"}, + {file = "rapidfuzz-3.14.5-cp313-cp313t-win_arm64.whl", hash = "sha256:aac0ad28c686a5e72b81668b906c030ee28050b244544b8af68e12fb32543895"}, + {file = "rapidfuzz-3.14.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1a31cc6d7d03e7318a0974c038959c59e19c752b81115f2e9138b3331cd64d45"}, + {file = "rapidfuzz-3.14.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0298d357e2bc59d572da4db0bc631009b6f8f6c9bc8c11e99a12b833f16b6575"}, + {file = "rapidfuzz-3.14.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59b3dba758661a318995655435c6ab20a04ade79fa51e75bc8dc107cac8df280"}, + {file = "rapidfuzz-3.14.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4900143d82071bdda533b00300c40b14b963ff826b3642cc463b6dd0f036585e"}, + {file = "rapidfuzz-3.14.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:feedf219672eef83ea6be6f3bb093bba396a8560fc75be85ba225f082903df0a"}, + {file = "rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419e4397a36e2665ec992d8d64c20ba4b2a42500c76ecadeca78a4f19cb9cc32"}, + {file = "rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:97131ab2be39043054ee28d99e09efe316e6d53449b7e962dfcf3c2de8b2b246"}, + {file = "rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:593c00dac4e30231c35bf3b4f1da8ec0998762e9e94425586a5d636fcd57f9d0"}, + {file = "rapidfuzz-3.14.5-cp314-cp314-win32.whl", hash = "sha256:0084b687b02b4e569b46d8d6d4ad25659528e6081cd6d067ca453a69035f07e4"}, + {file = "rapidfuzz-3.14.5-cp314-cp314-win_amd64.whl", hash = "sha256:5dfa89d78f22cd773054caff44827b846161a29f2dcf7e78b8f90d086621e502"}, + {file = "rapidfuzz-3.14.5-cp314-cp314-win_arm64.whl", hash = "sha256:67f3f9d2b444268ab53e47d31bab89954888d23c04c6789f2c727e51fe4b1d13"}, + {file = "rapidfuzz-3.14.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:77eac0526899b3c3ad1454bb2b03cdb491d67358ec8ef0c9c48bd61b632b431d"}, + {file = "rapidfuzz-3.14.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b9c6bd754d11f6e78ac54e3d86b4b11dc1ba2f13e5fc958899574532897f5a99"}, + {file = "rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:738c96944d076deeaff70e92b65696ab4f7ecb8081d7791c5403a3257dfaf8ff"}, + {file = "rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4c1bca487a17fe4226b4ffb2d30e799d2b274d692cffa76bd0746f56235fca3"}, + {file = "rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:af6a90a4ed2a48fa1a2d17e9d824e6c7c950bea5bad0b707c77fd55751e6bfef"}, + {file = "rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bf5018938208d4597b2e679a4f8cff9fd252f1df53583130ae56281a21801b64"}, + {file = "rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c0919d1f89ddf91129906705723118ea09754171e4116f5a5dbc667c7bc9b261"}, + {file = "rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:93d8da883a35116d6813432177f35e570db5b0a5e30ecb0cbd7cb39c815735df"}, + {file = "rapidfuzz-3.14.5-cp314-cp314t-win32.whl", hash = "sha256:0f23e37019ec07712d58976b1ab2b889f8649a7f7c2f626a2f34ea9139e79279"}, + {file = "rapidfuzz-3.14.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7d5ca9c7832e6879a707296d1463685f7c243a27846227044504741640caec66"}, + {file = "rapidfuzz-3.14.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3e91dcd2549b8f8d843f98ba03a17e01f3d8b72ce942adbbb6761bc58ffce813"}, + {file = "rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:578e6051f6d5e6200c259b47a103cf06bb875ab5814d17333fc0b5c290b22f4c"}, + {file = "rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbf1b8bb2695415b347f3727da1addca2acb82c9b97ac86bebf8b1bead1eb12d"}, + {file = "rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4a8f5cc84c7ad6bffa0e9947b33eb343ad66e6b53e94fe54378a5508c5ed53"}, + {file = "rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c6d85283629646fa87acc22c66b30ea9d4de7f6fdf887daa2e30fa041829b5"}, + {file = "rapidfuzz-3.14.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:dfef96543ced67d9513a422755db422ae1dc34dade0a1485e0b43e7342ed3ebf"}, + {file = "rapidfuzz-3.14.5.tar.gz", hash = "sha256:ba10ac57884ce82112f7ed910b67e7fb6072d8ef2c06e30dc63c0f604a112e0e"}, ] [package.extras] @@ -2363,85 +2363,6 @@ botocore = ">=1.37.4,<2.0a0" [package.extras] crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] -[[package]] -name = "scipy" -version = "1.17.1" -description = "Fundamental algorithms for scientific computing in Python" -optional = false -python-versions = ">=3.11" -groups = ["main"] -files = [ - {file = "scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec"}, - {file = "scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696"}, - {file = "scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee"}, - {file = "scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd"}, - {file = "scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c"}, - {file = "scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4"}, - {file = "scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444"}, - {file = "scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082"}, - {file = "scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff"}, - {file = "scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d"}, - {file = "scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8"}, - {file = "scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76"}, - {file = "scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086"}, - {file = "scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b"}, - {file = "scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21"}, - {file = "scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458"}, - {file = "scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb"}, - {file = "scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea"}, - {file = "scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87"}, - {file = "scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3"}, - {file = "scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c"}, - {file = "scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f"}, - {file = "scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d"}, - {file = "scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b"}, - {file = "scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6"}, - {file = "scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464"}, - {file = "scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950"}, - {file = "scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369"}, - {file = "scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448"}, - {file = "scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87"}, - {file = "scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a"}, - {file = "scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0"}, - {file = "scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce"}, - {file = "scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6"}, - {file = "scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e"}, - {file = "scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475"}, - {file = "scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50"}, - {file = "scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca"}, - {file = "scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c"}, - {file = "scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49"}, - {file = "scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717"}, - {file = "scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9"}, - {file = "scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b"}, - {file = "scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866"}, - {file = "scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350"}, - {file = "scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118"}, - {file = "scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068"}, - {file = "scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118"}, - {file = "scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19"}, - {file = "scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293"}, - {file = "scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6"}, - {file = "scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1"}, - {file = "scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39"}, - {file = "scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca"}, - {file = "scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad"}, - {file = "scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a"}, - {file = "scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4"}, - {file = "scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2"}, - {file = "scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484"}, - {file = "scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21"}, - {file = "scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0"}, -] - -[package.dependencies] -numpy = ">=1.26.4,<2.7" - -[package.extras] -dev = ["click (<8.3.0)", "cython-lint (>=0.12.2)", "mypy (==1.10.0)", "pycodestyle", "ruff (>=0.12.0)", "spin", "types-psutil", "typing_extensions"] -doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "linkify-it-py", "matplotlib (>=3.5)", "myst-nb (>=1.2.0)", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.2.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)", "tabulate"] -test = ["Cython", "array-api-strict (>=2.3.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest (>=8.0.0)", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - [[package]] name = "six" version = "1.17.0" @@ -2633,6 +2554,18 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] +[[package]] +name = "vcflib" +version = "1.0.1" +description = "An open-source Python library for VCF parsing and manipulation" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7,>=3.6" +groups = ["main"] +files = [ + {file = "vcflib-1.0.1-py3-none-any.whl", hash = "sha256:26437875d441926b9fc6de2ce455c3516e6d6a4cce2e5ce638a94b05c803e705"}, + {file = "vcflib-1.0.1.tar.gz", hash = "sha256:0b814ea000c54c9885d63713c2a631a82f4bac9fba0f42a2c9c57fa8bed04759"}, +] + [[package]] name = "zipp" version = "3.23.1" @@ -2657,4 +2590,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">= 3.11" -content-hash = "76a7a9eb1f2f55ae3e55005a34dc6419eee843b1d8d4c1788179c13fa0e220c5" +content-hash = "f5309634ba41416dcbd5e7c7cfe752a6f81a6fa14c15e2d29d093aed1d7a3d81" diff --git a/pyproject.toml b/pyproject.toml index 4172df5..331f196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "sentieon_cli" -version = "1.6.1" +version = "1.6.2" description = "Pipeline implementations for the Sentieon software" authors = [ {name = "Don Freed", email = "don.freed@sentieon.com"}, @@ -20,7 +20,7 @@ include = ["LICENSE"] dependencies = [ "packaging (>=23.2)", "mappy (>=2.30,<3.0)", - "scipy (>=1.17.1,<2.0.0)", + "vcflib (>=1.0.1,<2.0.0)", "levenshtein (>=0.27.3,<0.28.0)", ] diff --git a/sentieon_cli/_indel2cnv_utils.py b/sentieon_cli/_indel2cnv_utils.py new file mode 100644 index 0000000..f366cbd --- /dev/null +++ b/sentieon_cli/_indel2cnv_utils.py @@ -0,0 +1,113 @@ +"""Pure-Python replacements for ``Levenshtein.ratio`` and +``scipy.signal.find_peaks`` used by ``scripts/indel2cnv.py``. + +Kept in the package (not ``scripts/``) so pytest can import them — +``scripts/`` is excluded from collection by ``conftest.py``. +""" + +from __future__ import annotations + +from collections import Counter +from typing import Sequence + + +def ratio(a: str, b: str, threshold: float = 0.0) -> float: + """Return ``2 * LCS(a, b) / (len(a) + len(b))``. + + Equivalent to ``Levenshtein.ratio(a, b)`` (which uses insert/delete + cost 1 and substitution cost 2, making the underlying distance + equal to ``|a| + |b| - 2 * LCS(a, b)``). + + When ``threshold > 0`` and a cheap upper bound proves the ratio + cannot reach ``threshold``, returns ``0.0`` as a sentinel without + running the full LCS. Otherwise returns the exact ratio. + """ + la, lb = len(a), len(b) + if la == 0 and lb == 0: + return 1.0 + if la == 0 or lb == 0: + return 0.0 + if a == b: + return 1.0 + + tot = la + lb + + # Length-based upper bound: 2*LCS <= 2*min(|a|,|b|). + if threshold > 0.0 and 2 * min(la, lb) < threshold * tot: + return 0.0 + + # Character-multiset intersection is a true upper bound on 2*LCS. + ub_matches = sum((Counter(a) & Counter(b)).values()) + if ub_matches == 0: + return 0.0 + if threshold > 0.0 and (2.0 * ub_matches) < threshold * tot: + return 0.0 + + return 2.0 * _bit_parallel_lcs(a, b) / tot + + +def _bit_parallel_lcs(a: str, b: str) -> int: + """LCS length via the Allison-Dix bit-parallel algorithm. + + Each iteration is O(len(a) / word_size) big-int operations; Python + int ops exploit this implicitly. Puts the shorter string in ``a`` + to minimize bitmap width. + """ + if len(a) > len(b): + a, b = b, a + m = len(a) + + char_mask: dict[str, int] = {} + for i, ch in enumerate(a): + char_mask[ch] = char_mask.get(ch, 0) | (1 << i) + + full = (1 << m) - 1 + s = full + for ch in b: + t = char_mask.get(ch) + if t is None: + continue + u = s & t + if u == 0: + continue + s = ((s + u) | (s - u)) & full + return m - bin(s).count("1") + + +def find_peaks( + values: Sequence[float], *, distance: int, height: float +) -> list[int]: + """Return indices of local maxima >= ``height`` with no two kept + peaks closer than ``distance`` samples. + + Matches ``scipy.signal.find_peaks(x, distance=d, height=h)`` for the + one call site that uses it. Plateaus map to their midpoint index + (``(start + end) // 2``), matching scipy's behavior. Boundary + values are never peaks. + """ + n = len(values) + candidates: list[int] = [] + i = 1 + while i < n - 1: + if values[i] <= values[i - 1] or values[i] < height: + i += 1 + continue + j = i + while j + 1 < n and values[j + 1] == values[i]: + j += 1 + if j + 1 < n and values[j + 1] < values[i]: + candidates.append((i + j) // 2) + i = j + 1 + + if distance <= 1 or len(candidates) <= 1: + return candidates + + # Keep highest peaks first; drop any within `distance` of one + # already kept. Tie-break on index so earlier wins. + ordered = sorted(candidates, key=lambda k: (-values[k], k)) + kept: list[int] = [] + for p in ordered: + if all(abs(p - q) >= distance for q in kept): + kept.append(p) + kept.sort() + return kept diff --git a/sentieon_cli/command_strings.py b/sentieon_cli/command_strings.py index 8269348..86d00bf 100644 --- a/sentieon_cli/command_strings.py +++ b/sentieon_cli/command_strings.py @@ -8,10 +8,11 @@ import pathlib import shlex import subprocess as sp +import sys import typing from typing import Any, Dict, List, Optional, Union -from .driver import BaseDriver +from .driver import BaseDriver, Driver, ReadWriter from .logging import get_logger from .shell_pipeline import Command, InputProcSub, Pipeline @@ -115,7 +116,7 @@ def cmd_pyexec_vcf_mod_haploid_patch( if tech.upper() not in ("HIFI", "ONT"): raise ValueError(f"Unsupported tech '{tech}', must be 'HIFI' or 'ONT'") - cmd = f"sentieon pyexec {kwargs['vcf_mod_py']} -t {cores} " + cmd = f"{sys.executable} {kwargs['vcf_mod_py']} -t {cores} " cmd += "haploid_patch " cmd += f"--patch1 {hap1_patch} --patch2 {hap2_patch}" if tech.upper() == "HIFI": @@ -138,8 +139,7 @@ def cmd_pyexec_vcf_mod_patch( """Patch DNAscope and DNAscopeHP VCF files""" cmd = [ - "sentieon", - "pyexec", + sys.executable, str(kwargs["vcf_mod_py"]), "-t", str(cores), @@ -163,8 +163,7 @@ def cmd_pyexec_gvcf_combine( """Combine gVCF files""" cmd1 = Command( - "sentieon", - "pyexec", + sys.executable, str(kwargs["gvcf_combine_py"]), "-t", str(cores), @@ -196,8 +195,7 @@ def cmd_pyexec_vcf_mod_merge( """Merge haploid VCF files""" cmd = [ - "sentieon", - "pyexec", + sys.executable, kwargs["vcf_mod_py"], "-t", str(cores), @@ -227,8 +225,7 @@ def cmd_pyexec_vcf_mod_haploid_patch2( """Patch a single pair of haploid DNAscope and DNAscopeHP VCFs""" cmd = [ - "sentieon", - "pyexec", + sys.executable, str(kwargs["vcf_mod_py"]), "-t", str(cores), @@ -252,8 +249,7 @@ def cmd_pyexec_hybrid_select( slop_size: int = 1000, ) -> Pipeline: select_cmd = Command( - "sentieon", - "pyexec", + sys.executable, str(hybrid_select), "-v", str(vcf), @@ -302,8 +298,7 @@ def cmd_pyexec_hybrid_anno( threads: int, ) -> Pipeline: cmd = [ - "sentieon", - "pyexec", + sys.executable, str(hybrid_anno), "-v", str(vcf), @@ -900,30 +895,26 @@ def cmd_extract_kmc( reference: pathlib.Path, extract_model: pathlib.Path, tmp_dir: pathlib.Path, - threads=1, + rw_bam: pathlib.Path, + threads: int = 1, ) -> Pipeline: - merge_cmd = Command( - "samtools", - "merge", - "--reference", - str(reference), - "-@", - str(threads), - "-u", - "-o", - "/dev/stdout", - *[str(x) for x in input_aln], + # `rw_bam` must already be a symlink to /dev/stdout; + # ReadWriter writes to it, which resolves to the driver's stdout (= the + # pipe to pgutil extract). + driver = Driver( + reference=reference, + thread_count=threads, + input=input_aln, ) - view_cmd = Command( - "samtools", - "view", - "-@", - str(threads), - "-ub", - "-F", - "0xf00", - "-", + driver.add_algo( + ReadWriter( + rw_bam, + bam_compression=1, + output_flag_filter="0xf00:0", + ) ) + driver_cmd = Command(*driver.build_cmd()) + extract_cmd = Command( "sentieon", "pgutil", @@ -954,7 +945,7 @@ def cmd_extract_kmc( str(output_prefix), str(tmp_dir), ) - return Pipeline(merge_cmd, view_cmd, extract_cmd, kmc_cmd) + return Pipeline(driver_cmd, extract_cmd, kmc_cmd) def cmd_vg_haplotypes( @@ -1241,7 +1232,9 @@ def cmd_segdup_caller( sr_alignments: pathlib.Path, reference: pathlib.Path, sr_bundle: pathlib.Path, - genes="CFH,CFHR3,CYP11B1,CYP2D6,GBA,NCF1,PMS2,SMN1,STRC", + input_vcf: Optional[pathlib.Path] = None, + sex: Optional[str] = None, + genes: Optional[str] = None, ) -> Pipeline: cmd = [ "segdup-caller", @@ -1251,11 +1244,14 @@ def cmd_segdup_caller( str(reference), "--sr_model", str(sr_bundle), - "--genes", - genes, - "--outdir", - str(out_segdup), ] + if input_vcf is not None: + cmd.extend(["--input_vcf", str(input_vcf)]) + if sex is not None: + cmd.extend(["--sex", sex]) + if genes: + cmd.extend(["--genes", genes]) + cmd.extend(["--outdir", str(out_segdup)]) return Pipeline(Command(*cmd)) @@ -1373,6 +1369,7 @@ def cmd_minimap2_lift( minimap2_i: str = "16G", threads=1, mm2_xargs: List[str] = [], + lift_prefix: str = "GRCh38#0#", ) -> Pipeline: """Minimap2 alignment with pgutil lift""" mm2_cmd = Command( @@ -1403,6 +1400,8 @@ def cmd_minimap2_lift( str(reference_fasta) + ".fai", "-g", str(gfa_file), + "--prefix", + lift_prefix, ) sort_cmd = Command( diff --git a/sentieon_cli/dnascope.py b/sentieon_cli/dnascope.py index 09ed816..c4fa230 100644 --- a/sentieon_cli/dnascope.py +++ b/sentieon_cli/dnascope.py @@ -44,6 +44,7 @@ from .util import ( check_version, library_preloaded, + parse_rg_line, path_arg, split_alignment, total_memory, @@ -278,9 +279,7 @@ def validate(self) -> None: "and `--readgroups` arguments" ) sys.exit(2) - if not str(self.output_vcf).endswith(".vcf.gz"): - self.logger.error("The output file should end with '.vcf.gz'") - sys.exit(2) + self.validate_output_vcf() self.skip_multiqc = True if self.skip_metrics else self.skip_multiqc if not library_preloaded("libjemalloc.so"): @@ -308,6 +307,16 @@ def validate(self) -> None: ) sys.exit(2) + for rg in self.sr_readgroups: + try: + parse_rg_line(rg.replace(r"\t", "\t")) + except ValueError as e: + self.logger.error("Invalid --readgroups value '%s': %s", rg, e) + sys.exit(2) + + if self.sr_r1_fastq or self.align or self.collate_align: + self.validate_bwa_index() + def configure(self) -> None: self.configure_alignment() @@ -784,8 +793,7 @@ def dedup_and_metrics( rehead_job = Job( Pipeline( Command( - "sentieon", - "pyexec", + sys.executable, str(rehead_script), "--metrics_file", str(wgs_metrics), diff --git a/sentieon_cli/dnascope_hybrid.py b/sentieon_cli/dnascope_hybrid.py index 79184c1..958095b 100644 --- a/sentieon_cli/dnascope_hybrid.py +++ b/sentieon_cli/dnascope_hybrid.py @@ -325,9 +325,7 @@ def validate(self) -> None: self.collect_readgroups() self.validate_readgroups() - if not str(self.output_vcf).endswith(".vcf.gz"): - self.logger.error("The output file should end with '.vcf.gz'") - sys.exit(2) + self.validate_output_vcf() if not self.sr_aln and not self.sr_r1_fastq: self.logger.error("Please supply a short-read input file") sys.exit(2) @@ -354,6 +352,9 @@ def validate(self) -> None: ) sys.exit(2) + if self.sr_r1_fastq: + self.validate_bwa_index() + def validate_bundle(self) -> None: bundle_info_bytes = ar_load( str(self.model_bundle) + "/bundle_info.json" @@ -442,9 +443,14 @@ def collect_readgroups(self) -> None: self.all_readgroups[i][-1].append(parse_rg_line(rg_line)) for rg in self.sr_readgroups: self.all_readgroups[2].append([]) - self.all_readgroups[2][-1].append( - parse_rg_line(rg.replace(r"\t", "\t")) - ) + try: + parsed = parse_rg_line(rg.replace(r"\t", "\t")) + except ValueError as e: + self.logger.error( + "Invalid --sr_readgroups value '%s': %s", rg, e + ) + sys.exit(2) + self.all_readgroups[2][-1].append(parsed) def validate_readgroups(self) -> None: # Confirm that all readgroups have the same RGSM diff --git a/sentieon_cli/dnascope_longread.py b/sentieon_cli/dnascope_longread.py index 3261cc5..c256cd0 100644 --- a/sentieon_cli/dnascope_longread.py +++ b/sentieon_cli/dnascope_longread.py @@ -40,6 +40,7 @@ from .util import ( __version__, check_version, + parse_rg_line, path_arg, library_preloaded, vcf_id, @@ -307,9 +308,13 @@ def validate(self) -> None: "files" ) sys.exit(2) - if not str(self.output_vcf).endswith(".vcf.gz"): - self.logger.error("The output file should end with '.vcf.gz'") - sys.exit(2) + for rg in self.readgroups: + try: + parse_rg_line(rg.replace(r"\t", "\t")) + except ValueError as e: + self.logger.error("Invalid --readgroups value '%s': %s", rg, e) + sys.exit(2) + self.validate_output_vcf() if self.tech.upper() == "ONT": self.logger.info("Skipping CNV calling with ONT data") self.skip_cnv = True diff --git a/sentieon_cli/driver.py b/sentieon_cli/driver.py index 09d545f..c2bbb7f 100644 --- a/sentieon_cli/driver.py +++ b/sentieon_cli/driver.py @@ -271,9 +271,9 @@ def __init__( output_flag_filter: Optional[str] = None, ): self.output = output - bam_compression = bam_compression - cram_write_options = cram_write_options - output_flag_filter = output_flag_filter + self.bam_compression = bam_compression + self.cram_write_options = cram_write_options + self.output_flag_filter = output_flag_filter class InsertSizeMetricAlgo(BaseAlgo): diff --git a/sentieon_cli/pipeline.py b/sentieon_cli/pipeline.py index 68c4a03..39672c1 100644 --- a/sentieon_cli/pipeline.py +++ b/sentieon_cli/pipeline.py @@ -25,6 +25,8 @@ "multiqc": packaging.version.Version("1.18"), } +BWA_INDEX_SUFFIXES = (".amb", ".ann", ".bwt", ".pac", ".sa") + class BasePipeline(ABC): """A pipeline base class""" @@ -160,6 +162,14 @@ def validate_output_vcf(self) -> None: if not str(self.output_vcf).endswith(".vcf.gz"): self.logger.error("The output file should end with '.vcf.gz'") sys.exit(2) + assert self.output_vcf is not None + parent = self.output_vcf.resolve().parent + if not parent.is_dir(): + self.logger.error( + "The parent directory of the output VCF does not exist: %s", + parent, + ) + sys.exit(2) def validate_ref(self) -> None: # Confirm the presence of the reference index file @@ -172,6 +182,23 @@ def validate_ref(self) -> None: ) sys.exit(2) + def validate_bwa_index(self) -> None: + """Confirm the presence of BWA index files next to the reference.""" + missing = [ + str(self.reference) + suf + for suf in BWA_INDEX_SUFFIXES + if not os.path.isfile(str(self.reference) + suf) + ] + if missing: + self.logger.error( + "BWA index files are missing for reference %s: %s. Please " + "index the reference with 'sentieon bwa index %s'.", + self.reference, + ", ".join(missing), + self.reference, + ) + sys.exit(2) + @abstractmethod def configure(self) -> None: pass diff --git a/sentieon_cli/scripts/indel2cnv.py b/sentieon_cli/scripts/indel2cnv.py index c5fe617..68713f9 100755 --- a/sentieon_cli/scripts/indel2cnv.py +++ b/sentieon_cli/scripts/indel2cnv.py @@ -3,7 +3,6 @@ import math import io import vcflib -from scipy.signal import find_peaks from multiprocessing import Pool import argparse import logging @@ -13,8 +12,14 @@ import platform import mappy import os +import pathlib import re +# When launched as `python indel2cnv.py ...` from an arbitrary CWD, +# `sentieon_cli` is only importable if the package root is on sys.path. +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2])) +from sentieon_cli._indel2cnv_utils import find_peaks # noqa: E402 + # Convert long INDELs (from assembly-based SV calls) into tandem CNV truth events. # Pipeline: find_repeats (detect period) -> match_ref (locate repeat on reference) # -> extend array boundaries -> emit DEL/DUP with REFSTART/REFSTOP for cnv_eval. @@ -99,8 +104,8 @@ def find_repeats(var_seq, thresh): dist = [Levenshtein.ratio(var_seq[:n], var_seq[n:n*2]) for n in range(start, scan_limit, step)] if not dist or max(dist) < thresh: return [len(var_seq)] - peaks, _ = find_peaks(dist, distance=start//step, height=0.95) - if peaks.size == 0: + peaks = find_peaks(dist, distance=start//step, height=0.95) + if len(peaks) == 0: mid = length//2 if Levenshtein.ratio(var_seq[:mid], var_seq[mid:mid*2]) > thresh: peaks = [mid/step] diff --git a/sentieon_cli/sentieon_pangenome.py b/sentieon_cli/sentieon_pangenome.py index 6f94ab3..b17d3da 100644 --- a/sentieon_cli/sentieon_pangenome.py +++ b/sentieon_cli/sentieon_pangenome.py @@ -16,7 +16,7 @@ from . import command_strings as cmds from .archive import ar_load -from .base_pangenome import BasePangenome +from .base_pangenome import BasePangenome, SampleSex from .dag import DAG from .driver import ( AlignmentStat, @@ -34,6 +34,7 @@ MeanQualityByCycle, PangenomeSV, QualDistribution, + ReadWriter, WgsMetricsAlgo, ) from .job import Job @@ -65,6 +66,21 @@ "samtools": packaging.version.Version("1.16"), } +SEGDUP_MIN_VERSION = { + "segdup-caller": packaging.version.Version("0.5.1"), +} + +EXPANSION_MIN_VERSION = { + "ExpansionHunter": None, +} + +T1K_MIN_VERSION = { + "run-t1k": None, +} + +DEFAULT_T1K_HLA_LOCUS = "chr6:28510020-33480577" +DEFAULT_T1K_KIR_LOCUS = "chr19:53100000-55800000" + logger = get_logger(__name__) @@ -110,6 +126,16 @@ class SentieonPangenome(BasePangenome): "help": "Generate a gVCF output file.", "action": "store_true", }, + "pangenome_ref_name": { + "default": "GRCh38", + "help": "Reference name in the pangenome (GRCh38)", + }, + "pangenome_contig_prefix": { + "default": "GRCh38#0#", + "help": ( + "Prefix to strip from pangenome contig names (GRCh38#0#)" + ), + }, "skip_metrics": { "help": "Skip metrics collection and multiQC", "action": "store_true", @@ -118,6 +144,67 @@ class SentieonPangenome(BasePangenome): "help": "Skip multiQC report generation", "action": "store_true", }, + "segdup_caller": { + "nargs": "*", + "help": ( + "Call variants in difficult segmental duplications with " + "segdup-caller. Supply the flag with no arguments to run " + "the caller's default gene set. Supply a comma-separated " + "list of gene names (e.g. 'CFH,CYP2D6,SMN1') to restrict " + "calling to those genes." + ), + }, + "expansion_catalog": { + "help": ( + "An ExpansionHunter variant catalog. Required for short " + "tandem repeat expansion calling." + ), + "type": path_arg(exists=True, is_file=True), + }, + "t1k_hla_seq": { + "help": ( + "The DNA HLA seq FASTA file for T1K. Required for HLA " + "calling." + ), + "type": path_arg(exists=True, is_file=True), + }, + "t1k_hla_coord": { + "help": ( + "The DNA HLA coord FASTA file for T1K. Required for HLA " + "calling." + ), + "type": path_arg(exists=True, is_file=True), + }, + "t1k_hla_locus": { + "default": DEFAULT_T1K_HLA_LOCUS, + "help": ( + "Reference interval covering the HLA locus. Reads " + "overlapping this region are extracted before being " + f"passed to T1K (default: {DEFAULT_T1K_HLA_LOCUS})." + ), + }, + "t1k_kir_seq": { + "help": ( + "The DNA KIR seq FASTA file for T1K. Required for KIR " + "calling." + ), + "type": path_arg(exists=True, is_file=True), + }, + "t1k_kir_coord": { + "help": ( + "The DNA KIR coord FASTA file for T1K. Required for KIR " + "calling." + ), + "type": path_arg(exists=True, is_file=True), + }, + "t1k_kir_locus": { + "default": DEFAULT_T1K_KIR_LOCUS, + "help": ( + "Reference interval covering the KIR locus. Reads " + "overlapping this region are extracted before being " + f"passed to T1K (default: {DEFAULT_T1K_KIR_LOCUS})." + ), + }, # Hidden arguments "skip_contig_checks": { "help": argparse.SUPPRESS, @@ -152,8 +239,18 @@ def __init__(self) -> None: self.bed: Optional[pathlib.Path] = None self.call_svs = False self.gvcf = False + self.pangenome_ref_name = "GRCh38" + self.pangenome_contig_prefix = "GRCh38#0#" self.skip_metrics = False self.skip_multiqc = False + self.segdup_caller: Optional[List[str]] = None + self.expansion_catalog: Optional[pathlib.Path] = None + self.t1k_hla_seq: Optional[pathlib.Path] = None + self.t1k_hla_coord: Optional[pathlib.Path] = None + self.t1k_hla_locus: str = DEFAULT_T1K_HLA_LOCUS + self.t1k_kir_seq: Optional[pathlib.Path] = None + self.t1k_kir_coord: Optional[pathlib.Path] = None + self.t1k_kir_locus: str = DEFAULT_T1K_KIR_LOCUS self.skip_contig_checks: bool = False self.skip_pangenome_name_checks: bool = False self.skip_pop_vcf_id_check: bool = False @@ -184,6 +281,12 @@ def main(self, args: argparse.Namespace) -> None: executor = self.run(dag) self.check_execution(dag, executor) + if self.expansion_catalog or self.segdup_caller is not None: + self.get_sex(self.ploidy_json) + dag = self.build_second_dag() + executor = self.run(dag) + self.check_execution(dag, executor) + if not self.retain_tmpdir: shutil.rmtree(tmp_dir_str) @@ -209,6 +312,13 @@ def validate(self) -> None: ) sys.exit(2) + if self.r1_fastq: + self.validate_bwa_index() + + self.validate_segdup() + self.validate_expansion() + self.validate_t1k() + if not self.skip_version_check: for cmd, min_version in SENT_PANGENOME_MIN_VERSIONS.items(): if not check_version(cmd, min_version): @@ -276,6 +386,75 @@ def validate(self) -> None: ) sys.exit(2) + def validate_segdup(self) -> None: + if self.segdup_caller is None: + return + + if len(self.sample_input) > 1: + self.logger.error( + "`--segdup_caller` accepts only a single `--sample_input` " + "BAM/CRAM file." + ) + sys.exit(2) + + if self.skip_small_variants: + self.logger.error( + "`--segdup_caller` requires the small-variant VCF as " + "`--input_vcf` and cannot be combined with " + "`--skip_small_variants`." + ) + sys.exit(2) + + if not self.skip_version_check: + for cmd, min_version in SEGDUP_MIN_VERSION.items(): + if not check_version(cmd, min_version): + sys.exit(2) + + def validate_expansion(self) -> None: + if self.expansion_catalog is None: + return + + if len(self.sample_input) > 1: + self.logger.error( + "`--expansion_catalog` accepts only a single `--sample_input` " + "BAM/CRAM file." + ) + sys.exit(2) + + if not self.skip_version_check: + for cmd, min_version in EXPANSION_MIN_VERSION.items(): + if not check_version(cmd, min_version): + sys.exit(2) + + def validate_t1k(self) -> None: + hla_requested = self.t1k_hla_seq is not None or ( + self.t1k_hla_coord is not None + ) + if hla_requested and not (self.t1k_hla_seq and self.t1k_hla_coord): + self.logger.error( + "For HLA calling, both `--t1k_hla_seq` and `--t1k_hla_coord` " + "must be supplied." + ) + sys.exit(2) + + kir_requested = self.t1k_kir_seq is not None or ( + self.t1k_kir_coord is not None + ) + if kir_requested and not (self.t1k_kir_seq and self.t1k_kir_coord): + self.logger.error( + "For KIR calling, both `--t1k_kir_seq` and `--t1k_kir_coord` " + "must be supplied." + ) + sys.exit(2) + + if not (hla_requested or kir_requested): + return + + if not self.skip_version_check: + for cmd, min_version in T1K_MIN_VERSION.items(): + if not check_version(cmd, min_version): + sys.exit(2) + def validate_bundle(self) -> None: if not self.pop_vcf: self.logger.error("pop_vcf is required") @@ -334,12 +513,13 @@ def validate_bundle(self) -> None: ) sys.exit(2) - if self.call_svs and "cnv.model" not in bundle_members: - self.logger.error( - "The model bundle does not contain a 'cnv.model' file " - "required for CNV calling with `--call_svs`." + self.has_cnv_model = "cnv.model" in bundle_members + if self.call_svs and not self.has_cnv_model: + self.logger.warning( + "The model bundle does not contain a 'cnv.model' file. " + "CNV calling with CNVscope will be skipped; SV calling with " + "PangenomeSV will still run." ) - sys.exit(2) if not self.skip_pop_vcf_id_check and not self.dry_run: pop_vcf_id = vcf_id(self.pop_vcf) @@ -380,7 +560,13 @@ def collect_readgroups(self) -> None: if not self.readgroup: return - parsed_rg = parse_rg_line(self.readgroup.replace(r"\t", "\t")) + try: + parsed_rg = parse_rg_line(self.readgroup.replace(r"\t", "\t")) + except ValueError as e: + self.logger.error( + "Invalid --readgroup value '%s': %s", self.readgroup, e + ) + sys.exit(2) if not parsed_rg.get("ID"): self.logger.error( "Readgroup '%s' does not have a RGID tag", @@ -472,6 +658,17 @@ def build_first_dag(self) -> DAG: haplotype_dependencies.add(bwa_job) else: dnascope_bams = copy.deepcopy(self.sample_input) + # ReadWriter cannot write to /dev/stdout directly; pre-create a + # symlink so the driver writes to a real path that resolves to + # its stdout (the pipe to pgutil extract). + rw_bam = self.tmp_dir.joinpath("extract-kmc-rw.bam") + ln_job = Job( + Pipeline(Command("ln", "-sf", "/dev/stdout", str(rw_bam))), + "extract-kmc-symlink", + 1, + ) + dag.add_job(ln_job) + extract_kmc_job = Job( cmds.cmd_extract_kmc( kmer_prefix, @@ -480,12 +677,13 @@ def build_first_dag(self) -> DAG: self.reference, self.model_bundle.joinpath("extract.model"), self.tmp_dir, + rw_bam, threads=self.cores, ), "extract-kmc", self.cores, ) - dag.add_job(extract_kmc_job) + dag.add_job(extract_kmc_job, {ln_job}) haplotype_dependencies.add(extract_kmc_job) # vg haplotypes - create a sample-specific pangenome @@ -545,6 +743,48 @@ def build_first_dag(self) -> DAG: dnascope_bams.append(mm2_bam) cnv_input_bams = list(self.sample_input) + # Stash the short-read alignment for the second DAG + self.sr_alignment = cnv_input_bams[0] + + # Estimate ploidy when needed for sex-aware downstream tools + if self.expansion_catalog or self.segdup_caller is not None: + ploidy_job = self.build_ploidy_job( + self.ploidy_json, [cnv_input_bams[0]] + ) + dag.add_job(ploidy_job, cnvscope_deps) + + # T1K HLA/KIR calling + if self.t1k_hla_seq and self.t1k_hla_coord: + out_hla = pathlib.Path( + str(self.output_vcf).replace(".vcf.gz", "_hla") + ) + extract_job, t1k_job = self.build_t1k_jobs( + out_hla, + cnv_input_bams, + self.t1k_hla_seq, + self.t1k_hla_coord, + self.t1k_hla_locus, + "hla-wgs", + tag="hla", + ) + dag.add_job(extract_job, cnvscope_deps) + dag.add_job(t1k_job, {extract_job}) + if self.t1k_kir_seq and self.t1k_kir_coord: + out_kir = pathlib.Path( + str(self.output_vcf).replace(".vcf.gz", "_kir") + ) + extract_job, t1k_job = self.build_t1k_jobs( + out_kir, + cnv_input_bams, + self.t1k_kir_seq, + self.t1k_kir_coord, + self.t1k_kir_locus, + "kir-wgs", + tag="kir", + ) + dag.add_job(extract_job, cnvscope_deps) + dag.add_job(t1k_job, {extract_job}) + # DNAscope calling with bwa and mm2 input sv_vcf = None if self.call_svs: @@ -564,13 +804,14 @@ def build_first_dag(self) -> DAG: gfa_file=sample_gfa, ) dag.add_job(dnascope_job, dnascope_dependencies) - self._add_cnv_jobs( - dag, - sv_vcf, - cnv_input_bams, - cnvscope_deps, - dnascope_job, - ) + if self.has_cnv_model: + self._add_cnv_jobs( + dag, + sv_vcf, + cnv_input_bams, + cnvscope_deps, + dnascope_job, + ) return dag apply_dependencies = set() @@ -606,7 +847,7 @@ def build_first_dag(self) -> DAG: apply_job = self.build_dnamodelapply_job(transfer_vcf) dag.add_job(apply_job, apply_dependencies) - if self.call_svs and sv_vcf: + if self.call_svs and sv_vcf and self.has_cnv_model: self._add_cnv_jobs( dag, sv_vcf, @@ -680,7 +921,7 @@ def build_haplotypes_job( "--include-reference", "--diploid-sampling", "--set-reference", - "GRCh38", + self.pangenome_ref_name, ], ), "vg-haplotypes", @@ -697,6 +938,7 @@ def build_gfa_job( output_gfa, input_gbz, threads=self.cores, + reference_name=self.pangenome_ref_name, ), "vg-convert-gfa", 0, @@ -754,6 +996,7 @@ def build_minimap2_lift_job( "@RG\\t" + "\\t".join([f"{x[0]}:{x[1]}" for x in rg2.items()]), mm2_model, threads=self.cores, + lift_prefix=self.pangenome_contig_prefix, ), "mm2-lift", self.cores, @@ -869,8 +1112,7 @@ def build_metrics_job( rehead_job = Job( Pipeline( Command( - "sentieon", - "pyexec", + sys.executable, str(rehead_script), "--metrics_file", str(wgs_metrics), @@ -952,6 +1194,138 @@ def build_dnamodelapply_job(self, in_vcf) -> Job: self.cores, ) + def build_segdup_job( + self, + out_segdup: pathlib.Path, + sr_alignment: pathlib.Path, + input_vcf: pathlib.Path, + genes: Optional[str], + ) -> Job: + """Call variants in difficult SegDups""" + if not self.reference: + self.logger.error("reference is required") + sys.exit(2) + if not self.model_bundle: + self.logger.error("model_bundle is required") + sys.exit(2) + + sex = "male" if self.sample_sex == SampleSex.MALE else "female" + return Job( + cmds.cmd_segdup_caller( + out_segdup, + sr_alignment, + reference=self.reference, + sr_bundle=self.model_bundle, + input_vcf=input_vcf, + sex=sex, + genes=genes, + ), + "segdup-caller", + self.cores, + ) + + def build_second_dag(self) -> DAG: + """Build the second DAG for sex-aware downstream tools""" + self.logger.info("Building the second pangenome DAG") + dag = DAG() + + if self.expansion_catalog and self.sr_alignment: + out_expansions = pathlib.Path( + str(self.output_vcf).replace(".vcf.gz", "_expansion") + ) + expansion_job = self.build_expansion_job( + out_expansions, self.sr_alignment, self.expansion_catalog + ) + dag.add_job(expansion_job) + + # SegDup calling consumes the small-variant VCF and the inferred sex + if ( + self.segdup_caller is not None + and self.sr_alignment + and self.output_vcf + ): + out_segdup = pathlib.Path( + str(self.output_vcf).replace(".vcf.gz", "_segdups") + ) + genes = ",".join(self.segdup_caller) or None + segdup_job = self.build_segdup_job( + out_segdup, + self.sr_alignment, + self.output_vcf, + genes, + ) + dag.add_job(segdup_job) + + return dag + + def build_t1k_jobs( + self, + out_basename: pathlib.Path, + sr_alignments: List[pathlib.Path], + gene_seq: pathlib.Path, + gene_coord: pathlib.Path, + locus: str, + preset: str, + tag: str, + ) -> Tuple[Job, Job]: + """Extract reads at the T1K locus and genotype them with T1K""" + if not self.reference: + self.logger.error("reference is required") + sys.exit(2) + + # Extract reads overlapping the locus to a BAM file with ReadWriter + extracted_bam = self.tmp_dir.joinpath(f"sample_{tag}.bam") + driver = Driver( + reference=self.reference, + thread_count=self.cores, + input=sr_alignments, + interval=locus, + ) + driver.add_algo(ReadWriter(extracted_bam)) + extract_job = Job( + Pipeline(Command(*driver.build_cmd())), + f"t1k-{tag}-extract", + self.cores, + ) + + t1k_job = Job( + cmds.cmd_t1k( + out_basename, + extracted_bam, + gene_seq=gene_seq, + gene_coord=gene_coord, + preset=preset, + threads=self.cores, + ), + f"t1k-{tag}", + self.cores, + ) + return (extract_job, t1k_job) + + def build_expansion_job( + self, + out_expansions: pathlib.Path, + sr_alignment: pathlib.Path, + expansion_catalog: pathlib.Path, + ) -> Job: + """Identify repeat expansions""" + if not self.reference: + self.logger.error("reference is required") + sys.exit(2) + + return Job( + cmds.cmd_expansion_hunter( + out_expansions, + sr_alignment, + reference=self.reference, + variant_catalog=expansion_catalog, + sex="male" if self.sample_sex == SampleSex.MALE else "female", + threads=self.cores, + ), + "expansion-hunter", + self.cores, + ) + def _add_cnv_jobs( self, dag: DAG, @@ -1060,8 +1434,7 @@ def _build_indel2cnv_job( return Job( Pipeline( Command( - "sentieon", - "pyexec", + sys.executable, str(indel2cnv_script), str(self.reference), str(input_vcf), @@ -1087,8 +1460,7 @@ def _build_combine_cnv_job( return Job( Pipeline( Command( - "sentieon", - "pyexec", + sys.executable, str(combine_script), "--cnv", str(cnv_vcf), diff --git a/sentieon_cli/shard.py b/sentieon_cli/shard.py index ee1af4f..815f106 100644 --- a/sentieon_cli/shard.py +++ b/sentieon_cli/shard.py @@ -104,6 +104,14 @@ def vcf_contigs( kvpat = re.compile(r'(.*?)=(".*?"|.*?)(?:,|$)') cmd = ["bcftools", "view", "-h", str(in_vcf)] p = sp.run(cmd, capture_output=True, text=True) + if p.returncode != 0: + logger.error( + "`%s` failed with return code %d: %s", + " ".join(cmd), + p.returncode, + p.stderr.strip(), + ) + return {} contigs: Dict[str, Optional[int]] = {} for line in p.stdout.split("\n"): if not line.startswith("##contig"): diff --git a/sentieon_cli/util.py b/sentieon_cli/util.py index cf58108..c8c3f8f 100644 --- a/sentieon_cli/util.py +++ b/sentieon_cli/util.py @@ -187,9 +187,24 @@ def split_alignment(cores: int) -> List[str]: def parse_rg_line(rg_line: str) -> Dict[str, str]: - """Parse an @RG line""" + """Parse an @RG line. + + Each tab-separated field after ``@RG`` must be of the form + ``KEY:VALUE``. Raises ``ValueError`` with the offending token if a + field lacks the ``KEY:`` prefix (e.g. a bare ``HG002`` instead of + ``SM:HG002``). + """ tags = rg_line.split("\t")[1:] - return {tag.split(":", 1)[0]: tag.split(":", 1)[1] for tag in tags} + parsed: Dict[str, str] = {} + for tag in tags: + key, sep, value = tag.partition(":") + if not sep or not key: + raise ValueError( + f"malformed @RG field '{tag}' — expected 'KEY:VALUE' " + f"(e.g. 'SM:sample')" + ) + parsed[key] = value + return parsed def get_read_length_aln( @@ -238,6 +253,14 @@ def vcf_id(in_vcf: pathlib.Path) -> Optional[str]: """Collect the SentieonVcfID header""" cmd = ["bcftools", "view", "-h", str(in_vcf)] p = sp.run(cmd, capture_output=True, text=True) + if p.returncode != 0: + logger.error( + "`%s` failed with return code %d: %s", + " ".join(cmd), + p.returncode, + p.stderr.strip(), + ) + return None for line in p.stdout.split("\n"): if line.startswith("##SentieonVcfID="): i = line.index("=") diff --git a/tests/unit/test_indel2cnv_utils.py b/tests/unit/test_indel2cnv_utils.py new file mode 100644 index 0000000..9722b96 --- /dev/null +++ b/tests/unit/test_indel2cnv_utils.py @@ -0,0 +1,167 @@ +"""Golden-value tests for ``sentieon_cli._indel2cnv_utils``. + +Expected values were captured from ``Levenshtein.ratio`` (v0.27.3) and +``scipy.signal.find_peaks`` (v1.17.1). Keeping the deps as test-time +imports would defeat the point of this module, so the reference +outputs are frozen as literals here. +""" + +import os +import sys + +sys.path.insert( + 0, + os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")), +) + +import pytest + +from sentieon_cli._indel2cnv_utils import find_peaks, ratio + + +LONG_A = ( + "AAGCCCAATAAACCACTCTGACTGGCCGAATAGGGATATAGGCAACGACATGTGCGGCGACC" + "CTTGCGACAGTGACGCTTTCGCCGTTGCCTAAACCTATTTGAAGGAGTCTAGCAGCCGCAGT" + "AAGGCACAATACCTCGTCCGTGTTACCAGACCAAACAAGACGTCCTCTTCAATGTTTAAATG" + "ACCCTCTCGTCATAAAACCTTTCTACTATGTGTTCCGCAAGAATCAACAACTACAATGGCGC" + "GTCGTGAATAACGCGACGGCTGAGACGAACGGCGCGTGAATGAAGCGCTTAAACAGCTCAGG" + "AGCCAGTCCCCTACGTCGCATATCCTGGCCACTGGAGGTGAAGCGAATGGTATCGATACGTA" + "GGAGGTGTGCCTTCGTAGGCTGTTTCTCAGGACGCCCAACTATTCTTTCCAATCCTACATCT" + "GTTTCTTGCGTCGTAGCGGGACCCTCCATTGTTACTTATTAGGTTCTCGTTATGTCTCATAA" + "TCTC" +) +LONG_B = ( + "AAGCCCAATAAACCACTCTGCCTGGGGGAATAGGGTTATAGGCAACGACATGTGCGGGCTAC" + "CCTTGCGACAGTGACGCTTTCGCCAGTTGCCTAAACCTATTTAAAGGACTCTAGTAGCCGCA" + "ATAAGGCCCAATACCTCGTCGGTGTTACCAGACCAAACAAGACGTGCTCTTCAATGTTTAAA" + "TGACCCTCTCCTCATAAAACCTTTCTACTATGTGTTCCGCAAGAATCAACAACTGACCAGGG" + "CGCGTCGTGAATAACGCGACGGCTGAGACGAACGGCGCGTGAATGATAGCGCTTAAACAGCT" + "CAGGAGCCAGTCACCTACGTCGCATATCCTGGCCACTGGAGGTGAAGCGAATGGTATCGATA" + "CGTAGGAGGTGTGCCGTCGTACGCTGTTGTCTCAGGACGCCCAACTATTCTTTCCAATCCTA" + "CATCTGTTTCTTGCGTCGTAGCGGGACCCTCCATTGTTACTTATTAGGTTCTCGTTCAGTCT" + "CATAATCTC" +) +LONG_C = ( + "TCACAGAAGTGAGATTATGTCTCGTTTGGCAGTCTTGATGCTCGGGGGACACTTCTTTAAGC" + "TCGGTGTGGTGGGCACGACCCTGGACGCGCGACGAAGCTAAGTTTGCAGTAATTAACCGACA" + "TCTTTGTGAACCGACCCACATTTGACGGTACGCTACCGCAACGGTATGTGTTAATGGAACAG" + "ACTTGCTTATGTGGACGTTGTATAGGGATATTACGTTACGCGTTAACCGATACATACTGGTT" + "TCTCTCCAGTGGAGGTCTTGGTTGCCTCTAGTTTCTACGATATACTCATGGTAGTGTAACGC" + "ATAATCGAAGAGGGTCCTCCCATCTCCTGTGATGCATGGTGTGCTTACTGGGATGAATGCGC" + "CGCAAGTAGCAGGTCCCGGCGTGGATACCTGATAGATGGTGACTAGCATGTACAAGTAACCT" + "TGTCTATTGAGCTTCGAGGATGCATACAAGCCCACCCGCAGCCGCAACAGCGACGACTAATT" + "GATC" +) +RATIO_AB = 0.9572139303482587 +RATIO_AC = 0.636 + +TANDEM_UNIT = "ACGTACGAT" +TANDEM = TANDEM_UNIT * 6 +TANDEM_VAR = ( + TANDEM[: len(TANDEM) // 2] + "T" + TANDEM[len(TANDEM) // 2 + 1:] +) +TANDEM_RATIO = 0.9814814814814815 + + +class TestRatio: + """``ratio`` must match ``Levenshtein.ratio`` on the captured cases.""" + + @pytest.mark.parametrize( + "a,b,expected", + [ + ("ACGTACGT", "ACGTACGT", 1.0), + ("", "", 1.0), + ("ACGT", "", 0.0), + ("", "ACGT", 0.0), + ("AAAA", "CCCC", 0.0), + ("ACGT", "ACGGT", 0.8888888888888888), + ("ACGGT", "ACGT", 0.8888888888888888), + ("ACGT", "AGGT", 0.75), + ("ACGTACGT" * 4, "ACGTACGT" * 4, 1.0), + ("ACGTACGT" * 4, "CGTACGTA" * 4, 0.96875), + ("AACCGGTT", "AACCGGTG", 0.875), + (TANDEM, TANDEM_VAR, TANDEM_RATIO), + ], + ) + def test_golden_values(self, a, b, expected): + assert ratio(a, b) == pytest.approx(expected, abs=1e-12) + + def test_long_similar_dna(self): + assert ratio(LONG_A, LONG_B) == pytest.approx(RATIO_AB, abs=1e-12) + + def test_long_unrelated_dna(self): + assert ratio(LONG_A, LONG_C) == pytest.approx(RATIO_AC, abs=1e-12) + + def test_symmetry(self): + assert ratio(LONG_A, LONG_B) == ratio(LONG_B, LONG_A) + + def test_threshold_pass_returns_exact_value(self): + # Real ratio is above threshold — must return the exact value. + assert ratio(LONG_A, LONG_B, threshold=0.9) == pytest.approx( + RATIO_AB, abs=1e-12 + ) + + def test_threshold_fail_returns_below_threshold(self): + # Real ratio is below threshold — returned value must be < threshold. + result = ratio("AAAA", "CCCC", threshold=0.5) + assert result < 0.5 + + def test_threshold_zero_disables_abort(self): + # Whatever value threshold=0 returns must equal the no-threshold call. + assert ratio(LONG_A, LONG_C, threshold=0.0) == ratio(LONG_A, LONG_C) + + def test_threshold_very_asymmetric_lengths(self): + # Length-based upper bound rejects cheaply. + result = ratio("A" * 10, "A" * 10000, threshold=0.9) + assert result < 0.9 + + +class TestFindPeaks: + """``find_peaks`` must match ``scipy.signal.find_peaks(x, distance=d, + height=h)`` for the call pattern used by indel2cnv.py.""" + + @pytest.mark.parametrize( + "values,distance,height,expected", + [ + ([0.0, 0.5, 1.0, 0.5, 0.0], 1, 0.9, [2]), + ( + [0.0, 0.95, 0.0, 0.0, 0.0, 0.97, 0.0], + 2, 0.9, [1, 5], + ), + ([0.0, 0.95, 0.0, 0.97, 0.0], 3, 0.9, [3]), + ([0.0, 0.5, 0.0, 0.6, 0.0], 1, 0.9, []), + ([0.0, 0.95, 0.95, 0.0], 1, 0.9, [1]), + ([1.0, 0.9, 0.8], 1, 0.9, []), + ([0.8, 0.9, 1.0], 1, 0.9, []), + ( + [0.3, 0.5, 0.6, 0.95, 0.7, 0.6, 0.5, 0.96, 0.4, 0.3], + 2, 0.9, [3, 7], + ), + ], + ) + def test_golden_values(self, values, distance, height, expected): + assert find_peaks( + values, distance=distance, height=height + ) == expected + + def test_empty_input(self): + assert find_peaks([], distance=1, height=0.9) == [] + + def test_two_element_input(self): + # Neither element has both neighbors; never a peak. + assert find_peaks([0.0, 1.0], distance=1, height=0.5) == [] + + def test_left_edge_plateau_not_a_peak(self): + # scipy ignores plateaus without a strictly lower left neighbor. + assert find_peaks([0.95, 0.95, 0.0], distance=1, height=0.9) == [] + + def test_right_edge_plateau_not_a_peak(self): + # scipy ignores plateaus without a strictly lower right neighbor. + assert find_peaks([0.0, 0.95, 0.95], distance=1, height=0.9) == [] + + def test_wider_plateau_midpoint(self): + # Plateau of width 3 at indices 2..4; midpoint = 3. + assert find_peaks( + [0.0, 0.3, 0.95, 0.95, 0.95, 0.3, 0.0], + distance=1, height=0.9, + ) == [3] diff --git a/tests/unit/test_kmc_patch.py b/tests/unit/test_kmc_patch.py new file mode 100644 index 0000000..1589354 --- /dev/null +++ b/tests/unit/test_kmc_patch.py @@ -0,0 +1,33 @@ +""" +Tests for KMC patch check +""" + +import subprocess as sp +from unittest.mock import MagicMock, patch + +from sentieon_cli.util import check_kmc_patch + + +def test_check_kmc_patch_success(): + """Test check_kmc_patch returns True on success""" + with patch("subprocess.run") as mock_run: + mock_run.return_value.returncode = 0 + assert check_kmc_patch() is True + mock_run.assert_called_once() + args, kwargs = mock_run.call_args + assert "kmc" == args[0][0] + assert kwargs["input"] is not None + + +def test_check_kmc_patch_failure(): + """Test check_kmc_patch returns False on failure""" + with patch("subprocess.run") as mock_run: + mock_run.side_effect = sp.CalledProcessError(1, "kmc") + assert check_kmc_patch() is False + + +def test_check_kmc_patch_not_found(): + """Test check_kmc_patch returns False if kmc is missing""" + with patch("subprocess.run") as mock_run: + mock_run.side_effect = FileNotFoundError + assert check_kmc_patch() is False diff --git a/tests/unit/test_pipeline_validation.py b/tests/unit/test_pipeline_validation.py index 3585e07..b3ae579 100644 --- a/tests/unit/test_pipeline_validation.py +++ b/tests/unit/test_pipeline_validation.py @@ -43,6 +43,10 @@ def setup_method(self): for file_path in [self.mock_ref, self.mock_bam, self.mock_fastq, self.mock_bundle]: file_path.touch() + # Create BWA index files next to the reference + for suf in (".amb", ".ann", ".bwt", ".pac", ".sa"): + pathlib.Path(str(self.mock_ref) + suf).touch() + def test_valid_configuration_with_bam_input(self): """Test valid configuration with BAM input""" self.pipeline.output_vcf = self.mock_vcf @@ -443,5 +447,42 @@ def test_memory_calculation_logic(self): assert os.environ.get("bwt_max_mem") == "10G" +class TestValidateBwaIndex: + """Test BasePipeline.validate_bwa_index()""" + + BWA_SUFFIXES = (".amb", ".ann", ".bwt", ".pac", ".sa") + + def setup_method(self): + self.pipeline = DNAscopePipeline() + args = create_mock_args() + self.pipeline.setup_logging(args) + + self.temp_dir = tempfile.mkdtemp() + self.mock_ref = pathlib.Path(self.temp_dir) / "reference.fa" + self.mock_ref.touch() + self.pipeline.reference = self.mock_ref + + def _create_indexes(self, suffixes): + for suf in suffixes: + pathlib.Path(str(self.mock_ref) + suf).touch() + + def test_all_index_files_present(self): + self._create_indexes(self.BWA_SUFFIXES) + # Should not raise + self.pipeline.validate_bwa_index() + + def test_all_index_files_missing_raises(self): + with pytest.raises(SystemExit) as excinfo: + self.pipeline.validate_bwa_index() + assert excinfo.value.code == 2 + + def test_partial_index_files_missing_raises(self): + # All but .sa present + self._create_indexes(self.BWA_SUFFIXES[:-1]) + with pytest.raises(SystemExit) as excinfo: + self.pipeline.validate_bwa_index() + assert excinfo.value.code == 2 + + if __name__ == "__main__": pytest.main([__file__]) diff --git a/tests/unit/test_sentieon_pangenome.py b/tests/unit/test_sentieon_pangenome.py index b412389..8ecb709 100644 --- a/tests/unit/test_sentieon_pangenome.py +++ b/tests/unit/test_sentieon_pangenome.py @@ -86,6 +86,7 @@ def create_pipeline(self): pipeline.bam_readgroups = [{"ID": "rg1", "SM": "sample1"}] pipeline.fastq_readgroup = {} pipeline.tech = "Illumina" + pipeline.has_cnv_model = True pipeline.pcr_free = False return pipeline @@ -272,6 +273,42 @@ def test_cnv_jobs_with_skip_small_variants(self): assert "indel2cnv" in job_names assert "combine-cnv" in job_names + def test_call_svs_without_cnv_model(self): + """SVs run but CNV jobs are skipped when bundle lacks cnv.model""" + pipeline = self.create_pipeline() + pipeline.call_svs = True + pipeline.has_cnv_model = False + dag = pipeline.build_first_dag() + + job_names, all_jobs = self._get_all_job_names(dag) + + # PangenomeSV still runs + assert "dnascope-raw" in job_names + dnascope_job = next(j for j in all_jobs if j.name == "dnascope-raw") + cmd_str = str(dnascope_job.shell) + assert "--algo PangenomeSV" in cmd_str + + # CNV jobs are skipped + assert "cnvscope" not in job_names + assert "cnv-model-apply" not in job_names + assert "indel2cnv" not in job_names + assert "combine-cnv" not in job_names + + def test_call_svs_without_cnv_model_skip_small_variants(self): + """SV-only mode: SVs run but CNV jobs are skipped without cnv.model""" + pipeline = self.create_pipeline() + pipeline.skip_small_variants = True + pipeline.call_svs = True + pipeline.has_cnv_model = False + dag = pipeline.build_first_dag() + + job_names, _ = self._get_all_job_names(dag) + assert "dnascope-raw" in job_names + assert "cnvscope" not in job_names + assert "cnv-model-apply" not in job_names + assert "indel2cnv" not in job_names + assert "combine-cnv" not in job_names + def test_cnvscope_command(self): """Test CNVscope driver command has correct algo and model""" pipeline = self.create_pipeline() diff --git a/tests/unit/test_sentieon_pangenome_kmc_integration.py b/tests/unit/test_sentieon_pangenome_kmc_integration.py new file mode 100644 index 0000000..cfe4e61 --- /dev/null +++ b/tests/unit/test_sentieon_pangenome_kmc_integration.py @@ -0,0 +1,70 @@ +""" +Tests for KMC patch integration in SentieonPangenome +""" + +import sys +import unittest +from unittest.mock import MagicMock, patch + +from sentieon_cli.sentieon_pangenome import SentieonPangenome + + +class TestSentieonPangenomeKMCIntegration(unittest.TestCase): + def test_validate_kmc_patch_success(self): + """Test validation passes when KMC patch check succeeds""" + pipeline = SentieonPangenome() + pipeline.sample_input = ["sample.bam"] + pipeline.skip_version_check = False + pipeline.skip_pangenome_name_checks = True # skip irrelevant checks + pipeline.skip_contig_checks = True + pipeline.bed = "bed" # mock existence + pipeline.reference = "ref" + pipeline.pop_vcf = "pop" + pipeline.gbz = "ref.grch38.gbz" + pipeline.fai_data = {} + pipeline.pop_vcf_contigs = {} + + # Mock dependencies + pipeline.handle_arguments = MagicMock() + pipeline.setup_logging = MagicMock() + pipeline.validate_bundle = MagicMock() + pipeline.validate_fastq_rg = MagicMock() + pipeline.validate_output_vcf = MagicMock() + pipeline.validate_ref = MagicMock() + pipeline.collect_readgroups = MagicMock() + + with patch("sentieon_cli.sentieon_pangenome.check_version", return_value=True), \ + patch("sentieon_cli.sentieon_pangenome.check_kmc_patch", return_value=True) as mock_check, \ + patch("sys.exit") as mock_exit: + + pipeline.validate() + + mock_check.assert_called_with("kmc") + mock_exit.assert_not_called() + + def test_validate_kmc_patch_failure(self): + """Test validation fails when KMC patch check fails""" + pipeline = SentieonPangenome() + pipeline.sample_input = ["sample.bam"] + pipeline.skip_version_check = False + # other attributes mock + pipeline.validate_bundle = MagicMock() + pipeline.validate_fastq_rg = MagicMock() + pipeline.validate_output_vcf = MagicMock() + pipeline.validate_ref = MagicMock() + pipeline.collect_readgroups = MagicMock() + pipeline.bed = "bed" + pipeline.fai_data = {} + pipeline.pop_vcf_contigs = {} + + pipeline.logger = MagicMock() + + with patch("sentieon_cli.sentieon_pangenome.check_version", return_value=True), \ + patch("sentieon_cli.sentieon_pangenome.check_kmc_patch", return_value=False) as mock_check, \ + patch("sys.exit") as mock_exit: + + pipeline.validate() + + mock_check.assert_called_with("kmc") + pipeline.logger.error.assert_called() + mock_exit.assert_called_with(2) diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index 11e6a09..cfaf676 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -120,6 +120,10 @@ def create_dnascope_pipeline(self, **kwargs) -> DNAscopePipeline: # Create reference index self.fs.create_file("reference.fa.fai") + # Create BWA index files (required when aligning fastq or re-aligning) + for suf in (".amb", ".ann", ".bwt", ".pac", ".sa"): + self.fs.create_file("reference.fa" + suf) + # Default configuration pipeline.cores = 2 pipeline.dry_run = True @@ -198,6 +202,10 @@ def create_hybrid_pipeline(self, **kwargs) -> DNAscopeHybridPipeline: _fai = self.fs.create_file("reference.fa.fai") pipeline.model_bundle = self.fs.create_model_bundle("model.bundle", "DNAscope Hybrid") + # Create BWA index files (required when short-read fastq is supplied) + for suf in (".amb", ".ann", ".bwt", ".pac", ".sa"): + self.fs.create_file("reference.fa" + suf) + # Default configuration pipeline.cores = 2 pipeline.dry_run = True