From bf15655c83ea4e8a0ac24cb8eb8ac0d3e7446c04 Mon Sep 17 00:00:00 2001 From: Will Jones Date: Sat, 25 Jul 2026 09:22:35 -0700 Subject: [PATCH 01/29] chore: unify SDK versions and release tags on a single line (#3714) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python was versioned and tagged separately from the Rust, Java, and Node.js SDKs, and had drifted three minor versions ahead (0.36 vs 0.33). Users had no way to tell which Python version corresponded to which Rust or Node release, and the gap had no meaning behind it. This unifies the two tracks so there is one version and one tag for all four SDKs. ## Version The shared version is set to `0.37.0-beta.0`. Python continues its own sequence (highest published: 0.36 → 0.37) while Rust, Java, and Node.js jump 0.33 → 0.37 to meet it. Picking Python's next minor means Python users see no discontinuity at all, and only the other SDKs skip forward. Note that `main` trails the `release/v0.32` branch on both lines (main is at 0.32.0-beta.3 / 0.35.0-beta.3; the release branch carries 0.33.0-beta.0 / 0.36.0-beta.0), so 0.37 is chosen to clear the highest tag on either branch. Every index stays monotonic: | index | publishes | last published | next | |---|---|---|---| | PyPI | stable only | 0.34.0 | 0.37.0 | | Fury | previews | 0.36.0b0 | 0.37.0-beta.1 | | npm | both | 0.33.0-beta.0 | 0.37.0-beta.1 | | crates.io | stable only | 0.31.0 | 0.37.0 | | Maven | both | 0.33.0-beta.0 | 0.37.0-beta.1 | A one-time jump for three SDKs, versus explaining the offset indefinitely. ## Mechanism * `python/.bumpversion.toml` is removed. `python/Cargo.toml` — the source of the Python package version, since `pyproject.toml` declares `dynamic = ["version"]` — becomes a tracked file of the root config. Its `cargo update -p lancedb-python` pre-commit hook is dropped as redundant: `ci/update_lockfiles.sh` already refreshes every workspace member version in `Cargo.lock`. * `pypi-publish.yml` triggers on `v*` instead of `python-v*`, so one tag releases all four packages. `ci/bump_version.sh` and `make-release-commit.yml` lose their now-dead tag-prefix and per-language plumbing, including the `python` / `other` dispatch inputs. * The two byte-identical GH release jobs in `npm-publish.yml` and `pypi-publish.yml` are replaced by a single `gh-release.yml`. One release per tag, named `LanceDB vX.Y.Z`, instead of separate "Python LanceDB" and "Node/Rust LanceDB" releases for the same commit. The trade-off: there is no longer a way to ship a Python-only patch without also releasing crates.io, Maven, and npm. That is the cost of making drift structurally impossible. ## Beta releases marked "Latest" (#3666) Both GH release jobs used: ```yaml prerelease: ${{ contains('beta', github.ref) }} ``` The arguments are reversed. `contains(search, item)` asks whether *`search`* contains *`item`*, so this evaluated "does the literal string `'beta'` contain `refs/tags/python-v0.35.0-beta.2`?" — always `false`. Every beta was published as a full release, and GitHub awards "Latest" to the newest non-prerelease. The new workflow derives the flag from the parsed version rather than the raw ref, and sets `make_latest` explicitly: ```yaml prerelease: ${{ steps.extract_version.outputs.prerelease }} make_latest: ${{ steps.extract_version.outputs.prerelease == 'false' }} ``` npm was never affected (`--tag preview` uses correct bash), and PyPI already excludes pre-releases from resolution. This only fixes releases published from here on. Already-published betas need a one-time backfill: ```shell gh api --paginate /repos/lancedb/lancedb/releases \ --jq '.[] | select(.prerelease == false) | select(.tag_name | test("beta")) | .id' \ | xargs -I{} gh api -X PATCH /repos/lancedb/lancedb/releases/{} -F prerelease=true ``` ## Verification Ran `ci/bump_version.sh` end-to-end against this branch with the release tooling installed: * `preview` → tags `v0.37.0-beta.1` (previous tag `v0.33.0-beta.0` detected, `pre_n` bump) * `stable` → tags `v0.37.0` * Both paths update `.bumpversion.toml`, `rust/lancedb/Cargo.toml`, `nodejs/Cargo.toml`, `python/Cargo.toml`, `nodejs/package.json`, the 7 `nodejs/npm/*/package.json` files, both Java poms, and `docs/src/java/java.md` together * `check_breaking_changes.py` resolves the last stable as `v0.31.0`, so the minor-version gate passes All five touched workflows parse as valid YAML and the pre-commit hooks pass. ## Notes for review * This targets `main` only, so it takes effect at the next release-branch cut. The in-flight `release/v0.32` branch still carries `v0.33.0-beta.0` / `python-v0.36.0-beta.0`; if we want the imminent stable to be 0.37.0, this needs to be applied there too. * Historical `python-v*` tags are left alone. The changelog builder scans `^v`, which does not match them, so the first unified release's notes will compute `fromTag` from the Rust/Node line only — a one-time gap in the Python-side changelog. * Pre-existing and not addressed here: `ci/update_lockfiles.sh --amend` amends the commit that `bump-my-version` has already tagged, so the lockfile update lands outside the tag on stable releases. Fixes #3666 --- .bumpversion.toml | 9 ++- .github/workflows/cargo-publish.yml | 1 - .github/workflows/gh-release.yml | 85 +++++++++++++++++++++++ .github/workflows/make-release-commit.yml | 36 ++-------- .github/workflows/npm-publish.yml | 67 ------------------ .github/workflows/pypi-publish.yml | 78 ++------------------- Cargo.lock | 6 +- ci/bump_version.sh | 6 +- docs/src/java/java.md | 2 +- java/lancedb-core/pom.xml | 2 +- java/pom.xml | 2 +- nodejs/Cargo.toml | 2 +- nodejs/npm/darwin-arm64/package.json | 2 +- nodejs/npm/linux-arm64-gnu/package.json | 2 +- nodejs/npm/linux-arm64-musl/package.json | 2 +- nodejs/npm/linux-x64-gnu/package.json | 2 +- nodejs/npm/linux-x64-musl/package.json | 2 +- nodejs/npm/win32-arm64-msvc/package.json | 2 +- nodejs/npm/win32-x64-msvc/package.json | 2 +- nodejs/package-lock.json | 4 +- nodejs/package.json | 2 +- python/.bumpversion.toml | 49 ------------- python/Cargo.toml | 2 +- release_process.md | 46 ++++++------ rust/lancedb/Cargo.toml | 2 +- 25 files changed, 154 insertions(+), 261 deletions(-) create mode 100644 .github/workflows/gh-release.yml delete mode 100644 python/.bumpversion.toml diff --git a/.bumpversion.toml b/.bumpversion.toml index d36c4e158..57bcad5da 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,5 +1,5 @@ [tool.bumpversion] -current_version = "0.32.0-beta.3" +current_version = "0.37.0-beta.0" parse = """(?x) (?P0|[1-9]\\d*)\\. (?P0|[1-9]\\d*)\\. @@ -75,6 +75,13 @@ filename = "nodejs/Cargo.toml" replace = "\nversion = \"{new_version}\"" search = "\nversion = \"{current_version}\"" +# The Python package takes its version from here (pyproject.toml declares +# `dynamic = ["version"]`, so maturin reads it out of the crate manifest). +[[tool.bumpversion.files]] +filename = "python/Cargo.toml" +replace = "\nversion = \"{new_version}\"" +search = "\nversion = \"{current_version}\"" + # Java documentation [[tool.bumpversion.files]] filename = "docs/src/java/java.md" diff --git a/.github/workflows/cargo-publish.yml b/.github/workflows/cargo-publish.yml index 08f01d9a0..052fb1876 100644 --- a/.github/workflows/cargo-publish.yml +++ b/.github/workflows/cargo-publish.yml @@ -6,7 +6,6 @@ on: # We don't publish pre-releases for Rust. Crates.io is just a source # distribution, so we don't need to publish pre-releases. - "v*-beta*" - - "*-v*" # for example, python-vX.Y.Z env: # This env var is used by Swatinem/rust-cache@v2 for the cache diff --git a/.github/workflows/gh-release.yml b/.github/workflows/gh-release.yml new file mode 100644 index 000000000..683d55d71 --- /dev/null +++ b/.github/workflows/gh-release.yml @@ -0,0 +1,85 @@ +name: GitHub Release + +# All SDKs share one version, so a single `vX.Y.Z` tag produces a single GitHub +# release covering all of them. The per-package publish workflows (PyPI, NPM, +# Cargo, Maven) trigger off the same tag independently. + +on: + push: + tags: + - "v*" + +permissions: + contents: read + +jobs: + gh-release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + lfs: true + - name: Extract version + id: extract_version + env: + GITHUB_REF: ${{ github.ref }} + run: | + set -e + echo "Extracting tag and version from $GITHUB_REF" + if [[ $GITHUB_REF =~ refs/tags/v(.*) ]]; then + VERSION=${BASH_REMATCH[1]} + TAG=v$VERSION + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + else + echo "Failed to extract version from $GITHUB_REF" + exit 1 + fi + echo "Extracted version $VERSION from $GITHUB_REF" + if [[ $VERSION =~ beta ]]; then + echo "This is a beta release" + echo "prerelease=true" >> $GITHUB_OUTPUT + + # Get last release (that is not this one) + FROM_TAG=$(git tag --sort='version:refname' \ + | grep ^v \ + | grep -vF "$TAG" \ + | python ci/semver_sort.py v \ + | tail -n 1) + else + echo "This is a stable release" + echo "prerelease=false" >> $GITHUB_OUTPUT + # Get last stable tag (ignore betas) + FROM_TAG=$(git tag --sort='version:refname' \ + | grep ^v \ + | grep -vF "$TAG" \ + | grep -v beta \ + | python ci/semver_sort.py v \ + | tail -n 1) + fi + echo "Found from tag $FROM_TAG" + echo "from_tag=$FROM_TAG" >> $GITHUB_OUTPUT + - name: Create Release Notes + id: release_notes + uses: mikepenz/release-changelog-builder-action@v4 + with: + configuration: .github/release_notes.json + toTag: ${{ steps.extract_version.outputs.tag }} + fromTag: ${{ steps.extract_version.outputs.from_tag }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Create GH release + uses: softprops/action-gh-release@v2 + with: + # Marking betas as pre-releases keeps them from taking the "Latest" + # badge on the releases page. + prerelease: ${{ steps.extract_version.outputs.prerelease }} + make_latest: ${{ steps.extract_version.outputs.prerelease == 'false' }} + tag_name: ${{ steps.extract_version.outputs.tag }} + token: ${{ secrets.GITHUB_TOKEN }} + generate_release_notes: false + name: LanceDB v${{ steps.extract_version.outputs.version }} + body: ${{ steps.release_notes.outputs.changelog }} diff --git a/.github/workflows/make-release-commit.yml b/.github/workflows/make-release-commit.yml index 538c7f486..2ab69ee32 100644 --- a/.github/workflows/make-release-commit.yml +++ b/.github/workflows/make-release-commit.yml @@ -1,13 +1,14 @@ name: Create release commit -# This workflow increments versions, tags the version, and pushes it. +# This workflow increments the version, tags it, and pushes it. All SDKs share +# a single version, so one tag releases all of them. # When a tag is pushed, another workflow is triggered that creates a GH release # and uploads the binaries. This workflow is only for creating the tag. # This script will enforce that a minor version is incremented if there are any -# breaking changes since the last minor increment. However, it isn't able to -# differentiate between breaking changes in Node versus Python. If you wish to -# bypass this check, you can manually increment the version and push the tag. +# breaking changes since the last minor increment. A breaking change in any SDK +# bumps the minor version for all of them. If you wish to bypass this check, you +# can manually increment the version and push the tag. on: workflow_dispatch: inputs: @@ -24,16 +25,6 @@ on: options: - preview - stable - python: - description: 'Make a Python release' - required: true - default: true - type: boolean - other: - description: 'Make a Node/Rust/Java release' - required: true - default: true - type: boolean bump-minor: description: 'Bump minor version' required: true @@ -65,25 +56,12 @@ jobs: run: | git config user.name 'Lance Release' git config user.email 'lance-dev@lancedb.com' - - name: Bump Python version - if: ${{ inputs.python }} - working-directory: python - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Need to get the commit before bumping the version, so we can - # determine if there are breaking changes in the next step as well. - echo "COMMIT_BEFORE_BUMP=$(git rev-parse HEAD)" >> $GITHUB_ENV - - pip install bump-my-version PyGithub packaging - bash ../ci/bump_version.sh ${{ inputs.type }} ${{ inputs.bump-minor }} python-v - - name: Bump Node/Rust version - if: ${{ inputs.other }} + - name: Bump version env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | pip install bump-my-version PyGithub packaging - bash ci/bump_version.sh ${{ inputs.type }} ${{ inputs.bump-minor }} v $COMMIT_BEFORE_BUMP + bash ci/bump_version.sh ${{ inputs.type }} ${{ inputs.bump-minor }} bash ci/update_lockfiles.sh --amend - name: Push new version tag if: ${{ !inputs.dry_run }} diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 5707b085a..8a049ee0f 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -26,73 +26,6 @@ concurrency: cancel-in-progress: true jobs: - gh-release: - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - lfs: true - - name: Extract version - id: extract_version - env: - GITHUB_REF: ${{ github.ref }} - run: | - set -e - echo "Extracting tag and version from $GITHUB_REF" - if [[ $GITHUB_REF =~ refs/tags/v(.*) ]]; then - VERSION=${BASH_REMATCH[1]} - TAG=v$VERSION - echo "tag=$TAG" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - else - echo "Failed to extract version from $GITHUB_REF" - exit 1 - fi - echo "Extracted version $VERSION from $GITHUB_REF" - if [[ $VERSION =~ beta ]]; then - echo "This is a beta release" - - # Get last release (that is not this one) - FROM_TAG=$(git tag --sort='version:refname' \ - | grep ^v \ - | grep -vF "$TAG" \ - | python ci/semver_sort.py v \ - | tail -n 1) - else - echo "This is a stable release" - # Get last stable tag (ignore betas) - FROM_TAG=$(git tag --sort='version:refname' \ - | grep ^v \ - | grep -vF "$TAG" \ - | grep -v beta \ - | python ci/semver_sort.py v \ - | tail -n 1) - fi - echo "Found from tag $FROM_TAG" - echo "from_tag=$FROM_TAG" >> $GITHUB_OUTPUT - - name: Create Release Notes - id: release_notes - uses: mikepenz/release-changelog-builder-action@v4 - with: - configuration: .github/release_notes.json - toTag: ${{ steps.extract_version.outputs.tag }} - fromTag: ${{ steps.extract_version.outputs.from_tag }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create GH release - uses: softprops/action-gh-release@v2 - with: - prerelease: ${{ contains('beta', github.ref) }} - tag_name: ${{ steps.extract_version.outputs.tag }} - token: ${{ secrets.GITHUB_TOKEN }} - generate_release_notes: false - name: Node/Rust LanceDB v${{ steps.extract_version.outputs.version }} - body: ${{ steps.release_notes.outputs.changelog }} - build-lancedb: strategy: fail-fast: false diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 720079292..8825e7fb0 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -3,7 +3,7 @@ name: PyPI Publish on: push: tags: - - 'python-v*' + - 'v*' pull_request: # This should trigger a dry run (we skip the final publish step) paths: @@ -72,7 +72,7 @@ jobs: package-name: ${{ matrix.config.package_name }} rustflags: ${{ matrix.config.rustflags }} - uses: actions/upload-artifact@v7 - if: startsWith(github.ref, 'refs/tags/python-v') + if: startsWith(github.ref, 'refs/tags/v') with: name: wheels-linux-${{ matrix.config.package_name }}-${{ matrix.config.platform }}-${{ matrix.config.manylinux }} path: target/wheels/*.whl @@ -101,7 +101,7 @@ jobs: python-minor-version: 10 args: "--release --strip --target ${{ matrix.config.target }} --features fp16kernels" - uses: actions/upload-artifact@v7 - if: startsWith(github.ref, 'refs/tags/python-v') + if: startsWith(github.ref, 'refs/tags/v') with: name: wheels-mac-${{ matrix.config.target }} path: target/wheels/lancedb-*.whl @@ -127,14 +127,14 @@ jobs: python-minor-version: 10 args: "--release --strip" - uses: actions/upload-artifact@v7 - if: startsWith(github.ref, 'refs/tags/python-v') + if: startsWith(github.ref, 'refs/tags/v') with: name: wheels-windows path: target/wheels/lancedb-*.whl if-no-files-found: error publish: name: Publish wheels - if: startsWith(github.ref, 'refs/tags/python-v') + if: startsWith(github.ref, 'refs/tags/v') needs: [linux, mac, windows] runs-on: ubuntu-latest permissions: @@ -183,72 +183,6 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: target/wheels/ - gh-release: - if: startsWith(github.ref, 'refs/tags/python-v') - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - lfs: true - - name: Extract version - id: extract_version - env: - GITHUB_REF: ${{ github.ref }} - run: | - set -e - echo "Extracting tag and version from $GITHUB_REF" - if [[ $GITHUB_REF =~ refs/tags/python-v(.*) ]]; then - VERSION=${BASH_REMATCH[1]} - TAG=python-v$VERSION - echo "tag=$TAG" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - else - echo "Failed to extract version from $GITHUB_REF" - exit 1 - fi - echo "Extracted version $VERSION from $GITHUB_REF" - if [[ $VERSION =~ beta ]]; then - echo "This is a beta release" - - # Get last release (that is not this one) - FROM_TAG=$(git tag --sort='version:refname' \ - | grep ^python-v \ - | grep -vF "$TAG" \ - | python ci/semver_sort.py python-v \ - | tail -n 1) - else - echo "This is a stable release" - # Get last stable tag (ignore betas) - FROM_TAG=$(git tag --sort='version:refname' \ - | grep ^python-v \ - | grep -vF "$TAG" \ - | grep -v beta \ - | python ci/semver_sort.py python-v \ - | tail -n 1) - fi - echo "Found from tag $FROM_TAG" - echo "from_tag=$FROM_TAG" >> $GITHUB_OUTPUT - - name: Create Python Release Notes - id: python_release_notes - uses: mikepenz/release-changelog-builder-action@v4 - with: - configuration: .github/release_notes.json - toTag: ${{ steps.extract_version.outputs.tag }} - fromTag: ${{ steps.extract_version.outputs.from_tag }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create Python GH release - uses: softprops/action-gh-release@v2 - with: - prerelease: ${{ contains('beta', github.ref) }} - tag_name: ${{ steps.extract_version.outputs.tag }} - token: ${{ secrets.GITHUB_TOKEN }} - generate_release_notes: false - name: Python LanceDB v${{ steps.extract_version.outputs.version }} - body: ${{ steps.python_release_notes.outputs.changelog }} report-failure: name: Report Workflow Failure runs-on: ubuntu-latest @@ -256,7 +190,7 @@ jobs: permissions: contents: read issues: write - if: always() && failure() && startsWith(github.ref, 'refs/tags/python-v') + if: always() && failure() && startsWith(github.ref, 'refs/tags/v') steps: - uses: actions/checkout@v6 - uses: ./.github/actions/create-failure-issue diff --git a/Cargo.lock b/Cargo.lock index 26b64d182..f8bde1b00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5378,7 +5378,7 @@ dependencies = [ [[package]] name = "lancedb" -version = "0.32.0-beta.3" +version = "0.37.0-beta.0" dependencies = [ "ahash", "anyhow", @@ -5466,7 +5466,7 @@ dependencies = [ [[package]] name = "lancedb-nodejs" -version = "0.32.0-beta.3" +version = "0.37.0-beta.0" dependencies = [ "arrow-array", "arrow-buffer", @@ -5491,7 +5491,7 @@ dependencies = [ [[package]] name = "lancedb-python" -version = "0.35.0-beta.3" +version = "0.37.0-beta.0" dependencies = [ "arrow", "async-trait", diff --git a/ci/bump_version.sh b/ci/bump_version.sh index 1b3311c88..559e8ffdc 100644 --- a/ci/bump_version.sh +++ b/ci/bump_version.sh @@ -2,9 +2,9 @@ set -e RELEASE_TYPE=${1:-"stable"} BUMP_MINOR=${2:-false} -TAG_PREFIX=${3:-"v"} # Such as "python-v" -HEAD_SHA=${4:-$(git rev-parse HEAD)} +HEAD_SHA=$(git rev-parse HEAD) +readonly TAG_PREFIX="v" readonly SELF_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) PREV_TAG=$(git tag --sort='version:refname' | grep ^$TAG_PREFIX | python $SELF_DIR/semver_sort.py $TAG_PREFIX | tail -n 1) @@ -12,7 +12,7 @@ echo "Found previous tag $PREV_TAG" # Initially, we don't want to tag if we are doing stable, because we will bump # again later. See comment at end for why. -if [[ "$RELEASE_TYPE" == 'stable' ]]; then +if [[ "$RELEASE_TYPE" == 'stable' ]]; then BUMP_ARGS="--no-tag" fi diff --git a/docs/src/java/java.md b/docs/src/java/java.md index a7a010c99..891cc1674 100644 --- a/docs/src/java/java.md +++ b/docs/src/java/java.md @@ -14,7 +14,7 @@ Add the following dependency to your `pom.xml`: com.lancedb lancedb-core - 0.32.0-beta.3 + 0.37.0-beta.0 ``` diff --git a/java/lancedb-core/pom.xml b/java/lancedb-core/pom.xml index 9333f0cc6..eb47e25f3 100644 --- a/java/lancedb-core/pom.xml +++ b/java/lancedb-core/pom.xml @@ -8,7 +8,7 @@ com.lancedb lancedb-parent - 0.32.0-beta.3 + 0.37.0-beta.0 ../pom.xml diff --git a/java/pom.xml b/java/pom.xml index 721d61b93..a6ebf1165 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -6,7 +6,7 @@ com.lancedb lancedb-parent - 0.32.0-beta.3 + 0.37.0-beta.0 pom ${project.artifactId} LanceDB Java SDK Parent POM diff --git a/nodejs/Cargo.toml b/nodejs/Cargo.toml index dda9c681e..aef5fd54d 100644 --- a/nodejs/Cargo.toml +++ b/nodejs/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lancedb-nodejs" edition.workspace = true -version = "0.32.0-beta.3" +version = "0.37.0-beta.0" publish = false license.workspace = true description.workspace = true diff --git a/nodejs/npm/darwin-arm64/package.json b/nodejs/npm/darwin-arm64/package.json index f3d444fc6..618b02547 100644 --- a/nodejs/npm/darwin-arm64/package.json +++ b/nodejs/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-darwin-arm64", - "version": "0.32.0-beta.3", + "version": "0.37.0-beta.0", "os": ["darwin"], "cpu": ["arm64"], "main": "lancedb.darwin-arm64.node", diff --git a/nodejs/npm/linux-arm64-gnu/package.json b/nodejs/npm/linux-arm64-gnu/package.json index 9aabfec02..3eb4eeaba 100644 --- a/nodejs/npm/linux-arm64-gnu/package.json +++ b/nodejs/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-linux-arm64-gnu", - "version": "0.32.0-beta.3", + "version": "0.37.0-beta.0", "os": ["linux"], "cpu": ["arm64"], "main": "lancedb.linux-arm64-gnu.node", diff --git a/nodejs/npm/linux-arm64-musl/package.json b/nodejs/npm/linux-arm64-musl/package.json index 37f734fdc..7dbed5ae4 100644 --- a/nodejs/npm/linux-arm64-musl/package.json +++ b/nodejs/npm/linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-linux-arm64-musl", - "version": "0.32.0-beta.3", + "version": "0.37.0-beta.0", "os": ["linux"], "cpu": ["arm64"], "main": "lancedb.linux-arm64-musl.node", diff --git a/nodejs/npm/linux-x64-gnu/package.json b/nodejs/npm/linux-x64-gnu/package.json index cf7489468..733aef750 100644 --- a/nodejs/npm/linux-x64-gnu/package.json +++ b/nodejs/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-linux-x64-gnu", - "version": "0.32.0-beta.3", + "version": "0.37.0-beta.0", "os": ["linux"], "cpu": ["x64"], "main": "lancedb.linux-x64-gnu.node", diff --git a/nodejs/npm/linux-x64-musl/package.json b/nodejs/npm/linux-x64-musl/package.json index 46699201c..041a49cb4 100644 --- a/nodejs/npm/linux-x64-musl/package.json +++ b/nodejs/npm/linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-linux-x64-musl", - "version": "0.32.0-beta.3", + "version": "0.37.0-beta.0", "os": ["linux"], "cpu": ["x64"], "main": "lancedb.linux-x64-musl.node", diff --git a/nodejs/npm/win32-arm64-msvc/package.json b/nodejs/npm/win32-arm64-msvc/package.json index 433d2615e..2604d021e 100644 --- a/nodejs/npm/win32-arm64-msvc/package.json +++ b/nodejs/npm/win32-arm64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-win32-arm64-msvc", - "version": "0.32.0-beta.3", + "version": "0.37.0-beta.0", "os": [ "win32" ], diff --git a/nodejs/npm/win32-x64-msvc/package.json b/nodejs/npm/win32-x64-msvc/package.json index 6ff05abc1..2ee52df53 100644 --- a/nodejs/npm/win32-x64-msvc/package.json +++ b/nodejs/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-win32-x64-msvc", - "version": "0.32.0-beta.3", + "version": "0.37.0-beta.0", "os": ["win32"], "cpu": ["x64"], "main": "lancedb.win32-x64-msvc.node", diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 8cb52d0aa..2317c89a4 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lancedb/lancedb", - "version": "0.32.0-beta.3", + "version": "0.37.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lancedb/lancedb", - "version": "0.32.0-beta.3", + "version": "0.37.0-beta.0", "cpu": [ "x64", "arm64" diff --git a/nodejs/package.json b/nodejs/package.json index 749f4b816..e9b861ad1 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -11,7 +11,7 @@ "ann" ], "private": false, - "version": "0.32.0-beta.3", + "version": "0.37.0-beta.0", "main": "dist/index.js", "exports": { ".": "./dist/index.js", diff --git a/python/.bumpversion.toml b/python/.bumpversion.toml deleted file mode 100644 index 15b9eefa9..000000000 --- a/python/.bumpversion.toml +++ /dev/null @@ -1,49 +0,0 @@ -[tool.bumpversion] -current_version = "0.35.0-beta.3" -parse = """(?x) - (?P0|[1-9]\\d*)\\. - (?P0|[1-9]\\d*)\\. - (?P0|[1-9]\\d*) - (?:-(?P[a-zA-Z-]+)\\.(?P0|[1-9]\\d*))? -""" -serialize = [ - "{major}.{minor}.{patch}-{pre_l}.{pre_n}", - "{major}.{minor}.{patch}", -] -search = "{current_version}" -replace = "{new_version}" -regex = false -ignore_missing_version = false -ignore_missing_files = false -tag = true -sign_tags = false -tag_name = "python-v{new_version}" -tag_message = "Bump version: {current_version} → {new_version}" -allow_dirty = true -commit = true -message = "Bump version: {current_version} → {new_version}" -commit_args = "" -# bump-my-version >=1.4.0 rejects pre_commit_hooks containing shell syntax unless opted in. -allow_shell_hooks = true - -# Update Cargo.lock after version bump -pre_commit_hooks = [ - """ - cd python && cargo update -p lancedb-python - if git diff --quiet ../Cargo.lock; then - echo "Cargo.lock unchanged" - else - git add ../Cargo.lock - echo "Updated and staged Cargo.lock" - fi - """, -] - -[tool.bumpversion.parts.pre_l] -values = ["beta", "final"] -optional_value = "final" - -[[tool.bumpversion.files]] -filename = "Cargo.toml" -search = "\nversion = \"{current_version}\"" -replace = "\nversion = \"{new_version}\"" diff --git a/python/Cargo.toml b/python/Cargo.toml index 9a635cd64..a0e9394ec 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lancedb-python" -version = "0.35.0-beta.3" +version = "0.37.0-beta.0" publish = false edition.workspace = true description = "Python bindings for LanceDB" diff --git a/release_process.md b/release_process.md index 6df46962c..f223504a7 100644 --- a/release_process.md +++ b/release_process.md @@ -1,12 +1,17 @@ # Release process -There are five total packages we release. Four are the `lancedb` packages -for Python, Rust, Java, and Node.js. The other one is the legacy `vectordb` -package node.js. +We release four `lancedb` packages: Python, Rust, Java, and Node.js. -The Python package is versioned and released separately from the Rust, Java, and Node.js -ones. For Node.js the release process is shared between `lancedb` and -`vectordb` for now. +All four share a single version number, defined by `current_version` in +`.bumpversion.toml`. One `vX.Y.Z` tag releases all of them, so a breaking change +in any SDK bumps the minor version for every SDK. + +> [!NOTE] +> Python used to be versioned separately, under `python-vX.Y.Z` tags. It ran +> three minor versions ahead of the other SDKs, which made the two numbers hard +> to reason about. Both tracks were merged at `v0.37.0`: the Python line went +> `0.36` → `0.37` as usual, while Rust, Java, and Node.js jumped `0.33` → `0.37` +> to catch up. Tags before `v0.37.0` follow the old split scheme. ## Preview releases @@ -27,20 +32,21 @@ The release process uses a handful of GitHub actions to automate the process. ┌─────────────────────┐ │Create Release Commit│ └─┬───────────────────┘ - │ ┌────────────┐ ┌──►Python GH Release - ├──►(tag) python-vX.Y.Z ───►│PyPI Publish├─┤ - │ └────────────┘ └──►Python Wheels - │ - │ ┌───────────┐ - └──►(tag) vX.Y.Z ───┬──────►│NPM Publish├──┬──►Rust/Node GH Release - │ └───────────┘ │ - │ └──►NPM Packages - │ ┌─────────────┐ - ├──────►│Cargo Publish├───►Cargo Release - │ └─────────────┘ - │ ┌─────────────┐ - └──────►│Maven Publish├───►Java Maven Repo Release - └─────────────┘ + │ ┌──────────────┐ + └──►(tag) vX.Y.Z ─┬─►│GitHub Release├───►GH Release + │ └──────────────┘ + │ ┌────────────┐ + ├─►│PyPI Publish├─────►Python Wheels + │ └────────────┘ + │ ┌───────────┐ + ├─►│NPM Publish├──────►NPM Packages + │ └───────────┘ + │ ┌─────────────┐ + ├─►│Cargo Publish├────►Cargo Release + │ └─────────────┘ + │ ┌─────────────┐ + └─►│Maven Publish├────►Java Maven Repo Release + └─────────────┘ ``` To start a release, trigger a `Create Release Commit` action from diff --git a/rust/lancedb/Cargo.toml b/rust/lancedb/Cargo.toml index 7b016556f..ad6a4e2d6 100644 --- a/rust/lancedb/Cargo.toml +++ b/rust/lancedb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lancedb" -version = "0.32.0-beta.3" +version = "0.37.0-beta.0" edition.workspace = true description = "LanceDB: A serverless, low-latency vector database for AI applications" license.workspace = true From f655f62e0938395ab54caaa722addcb5351eb8fe Mon Sep 17 00:00:00 2001 From: Heng Ge Date: Sat, 25 Jul 2026 23:45:27 -0700 Subject: [PATCH 02/29] feat(query): add use_lsm to read MemWAL LSM data (#3489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What MemWAL LSM **read** support. When a table has an LSM write spec (`set_lsm_write_spec`), `merge_insert` upserts live in the MemWAL active/frozen memtables and flushed SSTables until an external compaction merges them into the base table, so a normal scan returns **stale** data. This routes reads through Lance's `LsmScanner` so queries also surface that in-flight data, deduplicated by primary key (newest generation wins). ## How - Adds a **`use_lsm: Option`** query flag, symmetric with the `merge_insert` flag: - **unset** — auto-route through the LSM scanner when the table carries a write spec - **`use_lsm(true)`** — force the LSM path; error if there is no spec - **`use_lsm(false)`** — read the base table only (the escape hatch) - Plain scan, single-column full-text search, and single-vector ANN all run through one `LsmScanner` (assembled from on-disk shard manifests plus the cached writer's in-memory memtables), so a `where` predicate is honored as a **prefilter** uniformly — including for vector search. - **Compaction-aware snapshots:** an SSTable generation is dropped only once it is both compacted into the base table and covered by the arm's base-index catch-up (`index_catchup`); plain scans use the compaction watermark alone. - Query shapes the scanner cannot honor hard-error with guidance to set `use_lsm(false)`: hybrid, multi/binary vectors, `with_row_id`, reranking, `order_by`, dynamic/Substrait projection or filters, `distance_range`, `use_index(false)`, postfilter, take-by-row-id/offset, reads from a time-traveled version, and an unmaintained or ambiguous (multiple) FTS/vector index. Namespace-pushdown queries fall back to local execution when a spec is present; WAL-only writers are handled. - Exposed across the Rust core and the Python (`use_lsm`) and TypeScript (`useLsm`) bindings, including `TakeQuery`. Rebased from Lance `7.2.0-beta.3` to `10.0.0-beta.3`. --- docs/src/js/classes/MergeInsertBuilder.md | 17 +- docs/src/js/classes/Query.md | 36 + docs/src/js/classes/TakeQuery.md | 23 + docs/src/js/classes/VectorQuery.md | 36 + nodejs/__test__/table.test.ts | 44 +- nodejs/lancedb/merge.ts | 18 +- nodejs/lancedb/query.ts | 38 + nodejs/src/merge.rs | 4 +- nodejs/src/query.rs | 15 + python/python/lancedb/_lancedb.pyi | 6 + python/python/lancedb/merge.py | 22 +- python/python/lancedb/query.py | 79 ++ python/python/lancedb/table.py | 4 +- python/python/tests/test_merge_insert_lsm.py | 484 +++++++++++- python/src/query.rs | 24 + python/src/table.rs | 6 +- rust/lancedb/src/lib.rs | 5 + rust/lancedb/src/query.rs | 40 + rust/lancedb/src/remote/table.rs | 10 + rust/lancedb/src/table/merge.rs | 445 ++++++++++- rust/lancedb/src/table/merge/lsm.rs | 80 +- rust/lancedb/src/table/query.rs | 114 ++- rust/lancedb/src/table/query/lsm.rs | 786 +++++++++++++++++++ 23 files changed, 2253 insertions(+), 83 deletions(-) create mode 100644 rust/lancedb/src/table/query/lsm.rs diff --git a/docs/src/js/classes/MergeInsertBuilder.md b/docs/src/js/classes/MergeInsertBuilder.md index ac0493bad..beb6cdfce 100644 --- a/docs/src/js/classes/MergeInsertBuilder.md +++ b/docs/src/js/classes/MergeInsertBuilder.md @@ -76,24 +76,23 @@ the query optimizer chooses a suboptimal path. *** -### useLsmWrite() +### useLsm() ```ts -useLsmWrite(useLsmWrite): MergeInsertBuilder +useLsm(enable): MergeInsertBuilder ``` -Controls whether the merge uses the MemWAL LSM write path. +Control MemWAL routing for this merge. By default (unset), a `mergeInsert` on a table with an LSM write spec is -routed through Lance's MemWAL shard writer, and a table without one uses -the standard path. Pass `false` to force the standard path even when a -spec is set. Pass `true` to require a spec — `mergeInsert` rejects if none -is installed. +routed through Lance's MemWAL shard writer, and a table without one uses the +standard path. #### Parameters -* **useLsmWrite**: `boolean` - Whether to use the LSM write path. +* **enable**: `boolean` + `true` forces MemWAL routing and errors if the table has no + LSM write spec. `false` forces the standard write path even when a spec is set. #### Returns diff --git a/docs/src/js/classes/Query.md b/docs/src/js/classes/Query.md index c4bc3ff23..6ebaebd75 100644 --- a/docs/src/js/classes/Query.md +++ b/docs/src/js/classes/Query.md @@ -497,6 +497,42 @@ ArrowTable. *** +### useLsm() + +```ts +useLsm(enable): this +``` + +Control MemWAL read routing for this query. + +By default (unset), when the table carries a MemWAL write spec (see +[Table#setLsmWriteSpec](Table.md#setlsmwritespec)), reads are routed through the LSM scanner so +they also return data written via the `mergeInsert` LSM path that has not yet +been compacted into the base table (the active/frozen in-memory memtables and +the flushed generations), deduplicated by primary key; a table without a spec +reads the base table. + +#### Parameters + +* **enable**: `boolean` + `true` forces the LSM scanner and errors if the table has no + MemWAL write spec. `false` bypasses the MemWAL and reads the base table only, + even when a spec is present. + Note: the LSM scanner does not support every query shape (e.g. reranking, + hybrid search, `orderBy`). On a MemWAL table those shapes error unless + `useLsm(false)` is set, because a base-only read would silently exclude + un-compacted MemWAL data. + +#### Returns + +`this` + +#### Inherited from + +`StandardQueryBase.useLsm` + +*** + ### where() ```ts diff --git a/docs/src/js/classes/TakeQuery.md b/docs/src/js/classes/TakeQuery.md index 0d658b32c..6ae2f9c8c 100644 --- a/docs/src/js/classes/TakeQuery.md +++ b/docs/src/js/classes/TakeQuery.md @@ -273,6 +273,29 @@ ArrowTable. *** +### useLsm() + +```ts +useLsm(enable): this +``` + +Control MemWAL read routing for this take query. + +`false` bypasses the MemWAL and reads the base table only — the escape hatch, +since take-by-row-id/offset is not supported on the LSM scanner and, on a +MemWAL table, auto-routes to it and errors otherwise. + +#### Parameters + +* **enable**: `boolean` + `false` reads the base table only. + +#### Returns + +`this` + +*** + ### withRowId() ```ts diff --git a/docs/src/js/classes/VectorQuery.md b/docs/src/js/classes/VectorQuery.md index af3317029..f9412c76d 100644 --- a/docs/src/js/classes/VectorQuery.md +++ b/docs/src/js/classes/VectorQuery.md @@ -746,6 +746,42 @@ ArrowTable. *** +### useLsm() + +```ts +useLsm(enable): this +``` + +Control MemWAL read routing for this query. + +By default (unset), when the table carries a MemWAL write spec (see +[Table#setLsmWriteSpec](Table.md#setlsmwritespec)), reads are routed through the LSM scanner so +they also return data written via the `mergeInsert` LSM path that has not yet +been compacted into the base table (the active/frozen in-memory memtables and +the flushed generations), deduplicated by primary key; a table without a spec +reads the base table. + +#### Parameters + +* **enable**: `boolean` + `true` forces the LSM scanner and errors if the table has no + MemWAL write spec. `false` bypasses the MemWAL and reads the base table only, + even when a spec is present. + Note: the LSM scanner does not support every query shape (e.g. reranking, + hybrid search, `orderBy`). On a MemWAL table those shapes error unless + `useLsm(false)` is set, because a base-only read would silently exclude + un-compacted MemWAL data. + +#### Returns + +`this` + +#### Inherited from + +`StandardQueryBase.useLsm` + +*** + ### where() ```ts diff --git a/nodejs/__test__/table.test.ts b/nodejs/__test__/table.test.ts index 44edaa093..15248f5cf 100644 --- a/nodejs/__test__/table.test.ts +++ b/nodejs/__test__/table.test.ts @@ -527,6 +527,14 @@ describe.each([arrow15, arrow16, arrow17, arrow18])( ); }); + it("should expose useLsm on takeRowIds as the base-only escape hatch", async () => { + await table.add([{ id: 1 }, { id: 2 }, { id: 3 }]); + // useLsm(false) is reachable on TakeQuery (the escape hatch for MemWAL tables, + // where take-by-row-id auto-routes to the LSM scanner and is rejected). + const res = await table.takeRowIds([0, 2]).useLsm(false).toArray(); + expect(res.map((r) => r.id)).toEqual([1, 3]); + }); + it("should throw for negative number in takeRowIds", () => { expect(() => table.takeRowIds([-1])).toThrow("Row id cannot be negative"); expect(() => table.takeRowIds([0, -5, 2])).toThrow( @@ -3199,14 +3207,14 @@ describe("LSM merge insert", () => { await table.closeLsmWriters(); }); - it("falls back to the standard path with useLsmWrite(false)", async () => { + it("falls back to the standard path with useLsm(false)", async () => { const conn = await connect(tmpDir.name); const table = await bucketTable(conn); const res = await table .mergeInsert("id") .whenNotMatchedInsertAll() - .useLsmWrite(false) + .useLsm(false) .execute([ { id: "b", value: 9 }, { id: "e", value: 5 }, @@ -3240,4 +3248,36 @@ describe("LSM merge insert", () => { .execute([{ id: "g", value: 7 }]), ).rejects.toThrow(); }); + + it("auto-routes reads through the MemWAL scanner", async () => { + const conn = await connect(tmpDir.name); + const table = await bucketTable(conn); // base ids "a", "b" + + await table + .mergeInsert("id") + .whenMatchedUpdateAll() + .whenNotMatchedInsertAll() + .execute([{ id: "c", value: 3 }]); + + // Default read auto-routes and includes the active memtable row. + const lsm = await table.query().toArray(); + expect(lsm.map((r) => r.id).sort()).toEqual(["a", "b", "c"]); + + // useLsm(false) bypasses the MemWAL and reads the base table only. + const baseOnly = await table.query().useLsm(false).toArray(); + expect(baseOnly.map((r) => r.id).sort()).toEqual(["a", "b"]); + }); + + it("reads the base table when no LSM spec is installed", async () => { + const conn = await connect(tmpDir.name); + const table = await conn.createEmptyTable( + "plain", + new arrow.Schema([new arrow.Field("id", new arrow.Utf8(), false)]), + ); + // No spec: default read and useLsm(false) both succeed against the base table. + await expect(table.query().toArray()).resolves.toBeDefined(); + await expect(table.query().useLsm(false).toArray()).resolves.toBeDefined(); + // useLsm(true) demands MemWAL routing; without a spec it errors. + await expect(table.query().useLsm(true).toArray()).rejects.toThrow(); + }); }); diff --git a/nodejs/lancedb/merge.ts b/nodejs/lancedb/merge.ts index 08321427f..30bde7281 100644 --- a/nodejs/lancedb/merge.ts +++ b/nodejs/lancedb/merge.ts @@ -88,21 +88,17 @@ export class MergeInsertBuilder { ); } /** - * Controls whether the merge uses the MemWAL LSM write path. + * Control MemWAL routing for this merge. * * By default (unset), a `mergeInsert` on a table with an LSM write spec is - * routed through Lance's MemWAL shard writer, and a table without one uses - * the standard path. Pass `false` to force the standard path even when a - * spec is set. Pass `true` to require a spec — `mergeInsert` rejects if none - * is installed. + * routed through Lance's MemWAL shard writer, and a table without one uses the + * standard path. * - * @param useLsmWrite - Whether to use the LSM write path. + * @param enable - `true` forces MemWAL routing and errors if the table has no + * LSM write spec. `false` forces the standard write path even when a spec is set. */ - useLsmWrite(useLsmWrite: boolean): MergeInsertBuilder { - return new MergeInsertBuilder( - this.#native.useLsmWrite(useLsmWrite), - this.#schema, - ); + useLsm(enable: boolean): MergeInsertBuilder { + return new MergeInsertBuilder(this.#native.useLsm(enable), this.#schema); } /** * Controls how an LSM merge checks that its input targets a single shard. diff --git a/nodejs/lancedb/query.ts b/nodejs/lancedb/query.ts index 7c9d96750..843a1276f 100644 --- a/nodejs/lancedb/query.ts +++ b/nodejs/lancedb/query.ts @@ -460,6 +460,30 @@ export class StandardQueryBase< this.doCall((inner: NativeQueryType) => inner.fastSearch()); return this; } + + /** + * Control MemWAL read routing for this query. + * + * By default (unset), when the table carries a MemWAL write spec (see + * {@link Table#setLsmWriteSpec}), reads are routed through the LSM scanner so + * they also return data written via the `mergeInsert` LSM path that has not yet + * been compacted into the base table (the active/frozen in-memory memtables and + * the flushed generations), deduplicated by primary key; a table without a spec + * reads the base table. + * + * @param enable - `true` forces the LSM scanner and errors if the table has no + * MemWAL write spec. `false` bypasses the MemWAL and reads the base table only, + * even when a spec is present. + * + * Note: the LSM scanner does not support every query shape (e.g. reranking, + * hybrid search, `orderBy`). On a MemWAL table those shapes error unless + * `useLsm(false)` is set, because a base-only read would silently exclude + * un-compacted MemWAL data. + */ + useLsm(enable: boolean): this { + this.doCall((inner: NativeQueryType) => inner.useLsm(enable)); + return this; + } } /** @@ -748,6 +772,20 @@ export class TakeQuery extends QueryBase { constructor(inner: NativeTakeQuery) { super(inner); } + + /** + * Control MemWAL read routing for this take query. + * + * `false` bypasses the MemWAL and reads the base table only — the escape hatch, + * since take-by-row-id/offset is not supported on the LSM scanner and, on a + * MemWAL table, auto-routes to it and errors otherwise. + * + * @param enable - `false` reads the base table only. + */ + useLsm(enable: boolean): this { + this.doCall((inner: NativeTakeQuery) => inner.useLsm(enable)); + return this; + } } /** A builder for LanceDB queries. diff --git a/nodejs/src/merge.rs b/nodejs/src/merge.rs index 1f9609160..ac8649419 100644 --- a/nodejs/src/merge.rs +++ b/nodejs/src/merge.rs @@ -51,9 +51,9 @@ impl NativeMergeInsertBuilder { } #[napi] - pub fn use_lsm_write(&self, use_lsm_write: bool) -> Self { + pub fn use_lsm(&self, enable: bool) -> Self { let mut this = self.clone(); - this.inner.use_lsm_write(use_lsm_write); + this.inner.use_lsm(enable); this } diff --git a/nodejs/src/query.rs b/nodejs/src/query.rs index 7266e05fc..3828023a9 100644 --- a/nodejs/src/query.rs +++ b/nodejs/src/query.rs @@ -168,6 +168,11 @@ impl Query { self.inner = self.inner.clone().with_row_id(); } + #[napi] + pub fn use_lsm(&mut self, enable: bool) { + self.inner = self.inner.clone().use_lsm(enable); + } + #[napi] pub fn order_by(&mut self, ordering: Option>) -> napi::Result<()> { let ordering = ordering.map(|ordering| { @@ -374,6 +379,11 @@ impl VectorQuery { self.inner = self.inner.clone().with_row_id(); } + #[napi] + pub fn use_lsm(&mut self, enable: bool) { + self.inner = self.inner.clone().use_lsm(enable); + } + #[napi] pub fn rerank( &mut self, @@ -479,6 +489,11 @@ impl TakeQuery { self.inner = self.inner.clone().with_row_id(); } + #[napi] + pub fn use_lsm(&mut self, enable: bool) { + self.inner = self.inner.clone().use_lsm(enable); + } + #[napi(catch_unwind)] pub async fn output_schema(&self) -> napi::Result { let schema = self.inner.output_schema().await.default_error()?; diff --git a/python/python/lancedb/_lancedb.pyi b/python/python/lancedb/_lancedb.pyi index c922aa69b..9c000a831 100644 --- a/python/python/lancedb/_lancedb.pyi +++ b/python/python/lancedb/_lancedb.pyi @@ -391,6 +391,7 @@ class Query: def fast_search(self): ... def with_row_id(self): ... def postfilter(self): ... + def use_lsm(self, enable: bool): ... def nearest_to(self, query_vec: pa.Array) -> VectorQuery: ... def nearest_to_text(self, query: dict) -> FTSQuery: ... def order_by(self, ordering: Optional[List[ColumnOrdering]]): ... @@ -407,6 +408,7 @@ class Query: class TakeQuery: def select(self, columns: List[str]): ... def with_row_id(self): ... + def use_lsm(self, enable: bool): ... async def output_schema(self) -> pa.Schema: ... async def execute(self) -> RecordBatchStream: ... async def explain_plan(self, verbose: Optional[bool]) -> str: ... @@ -425,6 +427,7 @@ class FTSQuery: def fast_search(self): ... def with_row_id(self): ... def postfilter(self): ... + def use_lsm(self, enable: bool): ... def get_query(self) -> str: ... def add_query_vector(self, query_vec: pa.Array) -> None: ... def nearest_to(self, query_vec: pa.Array) -> HybridQuery: ... @@ -452,6 +455,7 @@ class VectorQuery: def column(self, column: str): ... def distance_type(self, distance_type: str): ... def postfilter(self): ... + def use_lsm(self, enable: bool): ... def refine_factor(self, refine_factor: int): ... def nprobes(self, nprobes: int): ... def minimum_nprobes(self, minimum_nprobes: int): ... @@ -475,6 +479,7 @@ class HybridQuery: def fast_search(self): ... def with_row_id(self): ... def postfilter(self): ... + def use_lsm(self, enable: bool): ... def distance_type(self, distance_type: str): ... def refine_factor(self, refine_factor: int): ... def nprobes(self, nprobes: int): ... @@ -499,6 +504,7 @@ class PyQueryRequest: select: Optional[Union[str, List[str]]] fast_search: Optional[bool] with_row_id: Optional[bool] + use_lsm: Optional[bool] column: Optional[str] query_vector: Optional[List[pa.Array]] minimum_nprobes: Optional[int] diff --git a/python/python/lancedb/merge.py b/python/python/lancedb/merge.py index 386c592cd..3debed97b 100644 --- a/python/python/lancedb/merge.py +++ b/python/python/lancedb/merge.py @@ -37,7 +37,7 @@ class LanceMergeInsertBuilder(object): self._when_not_matched_by_source_condition_expr = None self._timeout = None self._use_index = True - self._use_lsm_write = None + self._use_lsm = None self._validate_single_shard = None def when_matched_update_all( @@ -113,22 +113,22 @@ class LanceMergeInsertBuilder(object): self._use_index = use_index return self - def use_lsm_write(self, use_lsm_write: bool) -> LanceMergeInsertBuilder: + def use_lsm(self, enable: bool) -> LanceMergeInsertBuilder: """ - Controls whether the merge uses the MemWAL LSM write path. + Control MemWAL routing for this merge. - By default (unset), a `merge_insert` on a table with an LSM write spec - is routed through Lance's MemWAL shard writer, and a table without one - uses the standard path. Pass `False` to force the standard path even - when a spec is set. Pass `True` to require a spec — `merge_insert` - raises an error if none is installed. + By default (unset), a `merge_insert` on a table with an LSM write spec is + routed through Lance's MemWAL shard writer, and a table without one uses + the standard path. Parameters ---------- - use_lsm_write: bool - Whether to use the LSM write path. + enable: bool + ``True`` forces MemWAL routing and errors if the table has no LSM + write spec. ``False`` forces the standard write path even when a spec + is set. """ - self._use_lsm_write = use_lsm_write + self._use_lsm = enable return self def validate_single_shard( diff --git a/python/python/lancedb/query.py b/python/python/lancedb/query.py index 9a730e35f..7063f08cc 100644 --- a/python/python/lancedb/query.py +++ b/python/python/lancedb/query.py @@ -778,6 +778,11 @@ class Query(pydantic.BaseModel): # if true, will only search the indexed data fast_search: Optional[bool] = None + # MemWAL LSM read routing: None auto-routes when the table carries a write + # spec, True forces the LSM scanner (errors without a spec), False reads the + # base table only + use_lsm: Optional[bool] = None + # size of the nearest neighbor list maintained during HNSW search ef: Optional[int] = None @@ -795,6 +800,9 @@ class Query(pydantic.BaseModel): query.full_text_query = req.full_text_search query.columns = req.select query.with_row_id = req.with_row_id + # use_lsm is a genuine tri-state (None / True / False); preserve it as-is + # so a round-tripped query keeps an explicit False. + query.use_lsm = req.use_lsm query.vector_column = req.column query.vector = req.query_vector query.distance_type = req.distance_type @@ -967,6 +975,7 @@ class LanceQueryBuilder(ABC): self._with_row_address = None self._fragments = None self._fragment_ids = None + self._use_lsm = None self._vector = None self._text = None self._ef = None @@ -1326,6 +1335,30 @@ class LanceQueryBuilder(ABC): self._fragment_ids = fragment_ids return self + def use_lsm(self, enable: bool) -> Self: + """Control MemWAL LSM read routing for this query. + + By default (unset), a query against a table with an LSM write spec is + routed through the LSM scanner so it also returns data written via the + ``merge_insert`` LSM path that has not yet been compacted into the base + table (active/frozen memtables + flushed generations); a table without a + spec reads the base table. + + Parameters + ---------- + enable : bool + ``True`` forces the LSM scanner and errors if the table has no LSM + write spec. ``False`` bypasses the MemWAL and reads the base table + only, even when a spec is present. + + Returns + ------- + LanceQueryBuilder + The LanceQueryBuilder object. + """ + self._use_lsm = enable + return self + def explain_plan(self, verbose: Optional[bool] = False) -> str: """Return the execution plan for this query. @@ -1788,6 +1821,7 @@ class LanceVectorQueryBuilder(LanceQueryBuilder): with_row_address=self._with_row_address, fragments=self._fragments, fragment_ids=self._fragment_ids, + use_lsm=self._use_lsm, offset=self._offset, fast_search=self._fast_search, ef=self._ef, @@ -2012,6 +2046,7 @@ class LanceFtsQueryBuilder(LanceQueryBuilder): with_row_address=self._with_row_address, fragments=self._fragments, fragment_ids=self._fragment_ids, + use_lsm=self._use_lsm, full_text_query=FullTextSearchQuery( query=self._query_with_phrase_semantics(), columns=self._fts_columns ), @@ -2078,6 +2113,7 @@ class LanceEmptyQueryBuilder(LanceQueryBuilder): with_row_address=self._with_row_address, fragments=self._fragments, fragment_ids=self._fragment_ids, + use_lsm=self._use_lsm, offset=self._offset, order_by=self._order_by, ) @@ -2655,6 +2691,9 @@ class LanceHybridQueryBuilder(LanceQueryBuilder): if self._with_row_id: self._vector_query.with_row_id(True) self._fts_query.with_row_id(True) + if self._use_lsm is not None: + self._vector_query.use_lsm(self._use_lsm) + self._fts_query.use_lsm(self._use_lsm) if self._phrase_query: self._fts_query.phrase_query(True) if self._distance_type: @@ -3231,6 +3270,27 @@ class AsyncStandardQuery(AsyncQueryBase): self._inner.fast_search() return self + def use_lsm(self, enable: bool) -> Self: + """ + Control MemWAL LSM read routing for this query. + + By default (unset), a query against a table with an LSM write spec (see + [AsyncTable.set_lsm_write_spec][lancedb.table.AsyncTable.set_lsm_write_spec]) + is routed through the LSM scanner so it also returns data written via the + ``merge_insert`` LSM path that has not yet been compacted into the base + table (the active/frozen in-memory memtables and the flushed generations), + deduplicated by primary key; a table without a spec reads the base table. + + Parameters + ---------- + enable : bool + ``True`` forces the LSM scanner and errors if the table has no LSM + write spec. ``False`` bypasses the MemWAL and reads the base table + only, even when a spec is present. + """ + self._inner.use_lsm(enable) + return self + def postfilter(self) -> Self: """ If this is called then filtering will happen after the search instead of @@ -3944,6 +4004,15 @@ class AsyncTakeQuery(AsyncQueryBase): def __init__(self, inner: LanceTakeQuery, table: Optional["AsyncTable"] = None): super().__init__(inner, table) + def use_lsm(self, enable: bool) -> "AsyncTakeQuery": + """Control MemWAL LSM read routing for this take query. + + ``False`` bypasses the MemWAL and reads the base table only — the escape + hatch, since take-by-row-id/offset is not supported on the LSM scanner. + """ + self._inner.use_lsm(enable) + return self + async def _plain_scan_to_pandas( self, blob_mode: BlobMode, @@ -4002,6 +4071,16 @@ class BaseQueryBuilder(object): self._inner.with_row_id() return self + def use_lsm(self, enable: bool) -> Self: + """ + Control MemWAL LSM read routing for this query. + + ``False`` bypasses the MemWAL and reads the base table only, the escape + hatch for shapes the LSM scanner cannot honor (e.g. take-by-row-id). + """ + self._inner.use_lsm(enable) + return self + def with_row_address(self, with_row_address: bool = True) -> Self: """ Include the _rowaddr column in scanner-backed plain query results. diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index 5f89f6308..f61a1f1cf 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -5363,6 +5363,8 @@ class AsyncTable: async_query = async_query.where(query.filter) if query.fast_search: async_query = async_query.fast_search() + if query.use_lsm is not None: + async_query = async_query.use_lsm(query.use_lsm) if query.with_row_id: async_query = async_query.with_row_id() if query.order_by: @@ -5483,7 +5485,7 @@ class AsyncTable: when_not_matched_by_source_condition_expr=merge._when_not_matched_by_source_condition_expr, timeout=merge._timeout, use_index=merge._use_index, - use_lsm_write=merge._use_lsm_write, + use_lsm=merge._use_lsm, validate_single_shard=merge._validate_single_shard, ), ) diff --git a/python/python/tests/test_merge_insert_lsm.py b/python/python/tests/test_merge_insert_lsm.py index abdfb306d..5674a05ab 100644 --- a/python/python/tests/test_merge_insert_lsm.py +++ b/python/python/tests/test_merge_insert_lsm.py @@ -9,6 +9,7 @@ import lancedb import pyarrow as pa import pytest from lancedb._lancedb import LsmWriteSpec +from lancedb.index import FTS, IvfPq SCHEMA = pa.schema( [ @@ -102,19 +103,35 @@ def test_lsm_merge_insert_identity(tmp_path): assert result.num_rows == 2 -def test_lsm_merge_insert_use_lsm_write_false(tmp_path): +def test_lsm_merge_insert_use_lsm_false(tmp_path): table = _bucket_table(tmp_path) # rows id = 1, 2, 3 - # use_lsm_write(False) opts out: the standard path runs and commits. + # use_lsm(False) opts out: the standard path runs and commits even with a spec. result = ( table.merge_insert("id") .when_not_matched_insert_all() - .use_lsm_write(False) + .use_lsm(False) .execute(_reader([3, 4, 5])) ) assert result.num_inserted_rows == 2 assert table.count_rows() == 5 +def test_lsm_merge_insert_use_lsm_true_without_spec_errors(tmp_path): + # A table with a primary key but no LSM write spec installed. + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table("t", _reader([1, 2, 3])) + table.set_unenforced_primary_key("id") + # use_lsm(True) demands MemWAL routing; without a spec it errors. + with pytest.raises(Exception, match="use_lsm"): + ( + table.merge_insert("id") + .when_matched_update_all() + .when_not_matched_insert_all() + .use_lsm(True) + .execute(_reader([3, 4, 5])) + ) + + def test_lsm_merge_insert_validate_single_shard_off(tmp_path): table = _bucket_table(tmp_path) result = ( @@ -127,19 +144,20 @@ def test_lsm_merge_insert_validate_single_shard_off(tmp_path): assert result.num_rows == 3 -def test_lsm_merge_insert_use_lsm_write_true_requires_spec(tmp_path): +def test_lsm_merge_insert_no_spec_uses_standard_path(tmp_path): # A table with a primary key but no LSM write spec installed. db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) table = db.create_table("t", _reader([1, 2, 3])) table.set_unenforced_primary_key("id") - with pytest.raises(Exception, match="use_lsm_write"): - ( - table.merge_insert("id") - .when_matched_update_all() - .when_not_matched_insert_all() - .use_lsm_write(True) - .execute(_reader([4])) - ) + # With no spec, a default merge_insert uses the standard path and commits. + result = ( + table.merge_insert("id") + .when_matched_update_all() + .when_not_matched_insert_all() + .execute(_reader([3, 4, 5])) + ) + assert result.num_inserted_rows == 2 + assert table.count_rows() == 5 def test_lsm_merge_insert_rejects_on_not_primary_key(tmp_path): @@ -194,3 +212,445 @@ async def test_async_lsm_merge_insert(tmp_path): result = await builder.execute(_reader([3, 4, 5])) assert result.num_rows == 3 await table.close_lsm_writers() + + +def _lsm_upsert(table, ids): + """Upsert ``ids`` (value = 0..n) through the LSM merge_insert path.""" + ( + table.merge_insert([]) + .when_matched_update_all() + .when_not_matched_insert_all() + .execute(_reader(ids)) + ) + + +def test_lsm_read_sees_active_memtable(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table("t", _reader([1, 2, 3])) # base ids 1,2,3 + table.set_unenforced_primary_key("id") + table.set_lsm_write_spec(LsmWriteSpec.unsharded()) + + _lsm_upsert(table, [4, 5]) # active memtable only, not committed to base + + # Default read auto-routes through the LSM scanner: base ∪ active memtable. + lsm = table.search().to_arrow() + assert sorted(lsm["id"].to_pylist()) == [1, 2, 3, 4, 5] + + # use_lsm(False) bypasses the MemWAL and reads the base table only. + base_only = table.search().use_lsm(False).to_arrow() + assert sorted(base_only["id"].to_pylist()) == [1, 2, 3] + + +def test_lsm_read_dedup_newest_wins(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table("t", _reader([1, 2, 3])) # id 2 -> value 1 + table.set_unenforced_primary_key("id") + table.set_lsm_write_spec(LsmWriteSpec.unsharded()) + + _lsm_upsert(table, [2, 3, 4]) # ids 2,3,4 -> values 0,1,2 + + lsm = table.search().to_arrow().sort_by("id") + assert lsm["id"].to_pylist() == [1, 2, 3, 4] + # id 1 from base (value 0); 2,3,4 from memtable (values 0,1,2). + assert lsm["value"].to_pylist() == [0, 0, 1, 2] + + +def test_lsm_read_without_spec_reads_base(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table("t", _reader([1, 2, 3])) + table.set_unenforced_primary_key("id") # no LSM write spec + + # No spec: default read and use_lsm(False) both read the base table, no error. + assert sorted(table.search().to_arrow()["id"].to_pylist()) == [1, 2, 3] + assert sorted(table.search().use_lsm(False).to_arrow()["id"].to_pylist()) == [ + 1, + 2, + 3, + ] + + +def test_lsm_read_unsupported_shape_errors_without_use_lsm_false(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table("t", _reader([1, 2, 3])) + table.set_unenforced_primary_key("id") + table.set_lsm_write_spec(LsmWriteSpec.unsharded()) + _lsm_upsert(table, [4]) + + # with_row_id is unsupported by the LSM scanner; on a MemWAL table the default + # (auto-routed) read hard-errors instead of silently reading a stale base. + with pytest.raises(Exception): + table.search().with_row_id(True).to_arrow() + + # use_lsm(False) is the escape hatch: it reads the base table only. + base = table.search().with_row_id(True).use_lsm(False).to_arrow() + assert sorted(base["id"].to_pylist()) == [1, 2, 3] + + +@pytest.mark.asyncio +async def test_async_lsm_read(tmp_path): + db = await lancedb.connect_async( + tmp_path, read_consistency_interval=timedelta(seconds=0) + ) + table = await db.create_table("t", _reader([1, 2, 3])) + await table.set_unenforced_primary_key("id") + await table.set_lsm_write_spec(LsmWriteSpec.unsharded()) + + builder = ( + table.merge_insert([]).when_matched_update_all().when_not_matched_insert_all() + ) + await builder.execute(_reader([4, 5])) + + arrow = await table.query().to_arrow() + assert sorted(arrow["id"].to_pylist()) == [1, 2, 3, 4, 5] + + +VECTOR_DIM = 8 + +VECTOR_SCHEMA = pa.schema( + [ + pa.field("id", pa.int64(), nullable=False), + pa.field("category", pa.utf8(), nullable=False), + pa.field("vector", pa.list_(pa.float32(), VECTOR_DIM), nullable=False), + ] +) + + +def _vector_reader(rows): + """Rows are ``(id, category, [f32; VECTOR_DIM])`` tuples.""" + batch = pa.RecordBatch.from_arrays( + [ + pa.array([row[0] for row in rows], type=pa.int64()), + pa.array([row[1] for row in rows], type=pa.utf8()), + pa.array([row[2] for row in rows], type=pa.list_(pa.float32(), VECTOR_DIM)), + ], + schema=VECTOR_SCHEMA, + ) + return pa.RecordBatchReader.from_batches(VECTOR_SCHEMA, [batch]) + + +def _vector_table(tmp_path): + """Base table whose vector column is indexed so its rows are visible to the LSM + vector scanner (the base arm uses ``fast_search`` — indexed data only), plus an + unsharded LSM spec that maintains that index for the memtable. + + Rows 1,2 are category ``a``, row 3 is ``b``, and 4..60 are filler ``c`` that + give the tiny IVF index enough data to train. + """ + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + rows = [ + ( + i, + "a" if i in (1, 2) else "b" if i == 3 else "c", + [float((i * 7 + j) % 13) for j in range(VECTOR_DIM)], + ) + for i in range(1, 61) + ] + table = db.create_table("t", _vector_reader(rows)) + table.set_unenforced_primary_key("id") + # num_partitions=1 makes the search exhaustive within the single partition + # (deterministic); num_bits=4 keeps PQ training viable on a tiny dataset. + table.create_index( + "vector", config=IvfPq(num_partitions=1, num_sub_vectors=2, num_bits=4) + ) + index_name = table.list_indices()[0].name + table.set_lsm_write_spec( + LsmWriteSpec.unsharded().with_maintained_indexes([index_name]) + ) + return table + + +def _vector_upsert(table, rows): + ( + table.merge_insert([]) + .when_matched_update_all() + .when_not_matched_insert_all() + .execute(_vector_reader(rows)) + ) + + +def test_lsm_read_vector_sees_memtable(tmp_path): + table = _vector_table(tmp_path) + # id 1000 lands in the active memtable, not committed to the base table. + _vector_upsert(table, [(1000, "a", [1.0] * VECTOR_DIM)]) + + query = [1.0] * VECTOR_DIM + # Vector search auto-routes through the LSM scanner: indexed base ∪ memtable. + ids = set(table.search(query).limit(100).to_arrow()["id"].to_pylist()) + assert {1, 2, 3} <= ids # indexed base rows + assert 1000 in ids # in-flight memtable row + + # use_lsm(False) bypasses the MemWAL, so the in-flight row is not visible. + base_ids = set( + table.search(query).use_lsm(False).limit(100).to_arrow()["id"].to_pylist() + ) + assert {1, 2, 3} <= base_ids + assert 1000 not in base_ids + + +def test_lsm_read_vector_prefilter(tmp_path): + table = _vector_table(tmp_path) + # in-flight rows in both categories. + _vector_upsert( + table, [(1000, "a", [1.0] * VECTOR_DIM), (1001, "b", [1.0] * VECTOR_DIM)] + ) + + query = [1.0] * VECTOR_DIM + # The `where` predicate must apply as a prefilter across base ∪ memtable — + # regression test for the vector arm silently dropping the filter. + rows = table.search(query).where("category = 'a'").limit(100).to_arrow() + assert set(rows["id"].to_pylist()) == {1, 2, 1000} + assert set(rows["category"].to_pylist()) == {"a"} + + # Sanity: without the filter, other categories are returned too. + unfiltered = set(table.search(query).limit(100).to_arrow()["category"].to_pylist()) + assert unfiltered != {"a"} + + +def test_lsm_read_plain_prefilter(tmp_path): + table = _vector_table(tmp_path) + _vector_upsert( + table, [(1000, "a", [1.0] * VECTOR_DIM), (1001, "b", [1.0] * VECTOR_DIM)] + ) + + # Plain scan + filter over base ∪ memtable: base 'a' rows 1,2 and memtable 1000. + rows = table.search().where("category = 'a'").to_arrow() + assert set(rows["id"].to_pylist()) == {1, 2, 1000} + + +FTS_SCHEMA = pa.schema( + [ + pa.field("id", pa.int64(), nullable=False), + pa.field("text", pa.utf8(), nullable=False), + ] +) + + +def _fts_reader(rows): + """Rows are ``(id, text)`` tuples.""" + batch = pa.RecordBatch.from_arrays( + [ + pa.array([row[0] for row in rows], type=pa.int64()), + pa.array([row[1] for row in rows], type=pa.utf8()), + ], + schema=FTS_SCHEMA, + ) + return pa.RecordBatchReader.from_batches(FTS_SCHEMA, [batch]) + + +def test_lsm_read_fts_sees_memtable(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table( + "t", + _fts_reader( + [ + (1, "the quick brown fox"), + (2, "lazy dog sleeps"), + (3, "quick red fox"), + ] + ), + ) + table.set_unenforced_primary_key("id") + # Native FTS index (tantivy is not compatible with the LSM memtable index). + table.create_index("text", config=FTS()) + index_name = table.list_indices()[0].name + table.set_lsm_write_spec( + LsmWriteSpec.unsharded().with_maintained_indexes([index_name]) + ) + + # in-flight doc 4 lands in the memtable's maintained FTS index. + ( + table.merge_insert([]) + .when_matched_update_all() + .when_not_matched_insert_all() + .execute(_fts_reader([(4, "brown fox jumps")])) + ) + + # Full-text search auto-routes through the LSM scanner: base ∪ memtable. + ids = set( + table.search("fox", query_type="fts", fts_columns="text") + .limit(10) + .to_arrow()["id"] + .to_pylist() + ) + assert ids == {1, 3, 4} + + # Prefilter restricts the FTS results across both tiers. + filtered = set( + table.search("fox", query_type="fts", fts_columns="text") + .where("id > 1") + .limit(10) + .to_arrow()["id"] + .to_pylist() + ) + assert filtered == {3, 4} + + +def test_lsm_read_vector_unsupported_knobs_error(tmp_path): + table = _vector_table(tmp_path) + _vector_upsert(table, [(1000, "a", [1.0] * VECTOR_DIM)]) + query = [1.0] * VECTOR_DIM + + # distance_range and use_index(False) change the vector result set/mode, which + # the LSM scanner can't honor, so it hard-errors instead of silently returning + # wrong results (matching the prefilter / unsupported-shape contract). + with pytest.raises(Exception, match="distance_range"): + table.search(query).distance_range(0.0, 0.5).to_arrow() + with pytest.raises(Exception, match="use_index"): + table.search(query).bypass_vector_index().to_arrow() + + # use_lsm(False) is the escape hatch: the base-only standard path honors them. + base = table.search(query).distance_range(0.0, 100.0).use_lsm(False).to_arrow() + assert 1000 not in set(base["id"].to_pylist()) + + +def test_lsm_read_vector_limit_offset(tmp_path): + table = _vector_table(tmp_path) + _vector_upsert(table, [(1000, "a", [1.0] * VECTOR_DIM)]) + query = [1.0] * VECTOR_DIM + # Lance's plan_vector over-fetches k + offset internally, so paging is correct: + # the second page is a full page (not truncated) and disjoint from the first. + page1 = table.search(query).limit(3).offset(0).to_arrow()["id"].to_pylist() + page2 = table.search(query).limit(3).offset(3).to_arrow()["id"].to_pylist() + assert len(page1) == 3 + # If k ignored offset, page2 would be empty (limit - offset = 0); a full second + # page that differs from the first proves offset widens the candidate pool. + assert len(page2) == 3 + assert set(page1) != set(page2) + + +def test_lsm_read_vector_postfilter_errors(tmp_path): + table = _vector_table(tmp_path) + _vector_upsert(table, [(1000, "a", [1.0] * VECTOR_DIM)]) + query = [1.0] * VECTOR_DIM + # The LSM scanner always prefilters; a requested postfilter changes results, so + # it hard-errors rather than silently prefiltering. + with pytest.raises(Exception, match="postfilter"): + table.search(query).where("category = 'a'").postfilter().to_arrow() + + +def test_lsm_read_projection_excludes_pk(tmp_path): + table = _vector_table(tmp_path) + _vector_upsert(table, [(1000, "a", [1.0] * VECTOR_DIM)]) + # Selecting only 'category' must not leak the 'id' primary key Lance appends + # internally for dedup. + rows = table.search().select(["category"]).where("category = 'a'").to_arrow() + assert rows.column_names == ["category"] + + +def test_lsm_read_fts_unmaintained_index_errors(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table("t", _fts_reader([(1, "quick fox"), (2, "lazy dog")])) + table.set_unenforced_primary_key("id") + table.create_index("text", config=FTS()) + # No maintained indexes: the active memtable FTS arm cannot serve un-compacted + # docs, so the search would silently omit them — reject instead. + table.set_lsm_write_spec(LsmWriteSpec.unsharded()) + with pytest.raises(Exception, match="maintained"): + table.search("fox", query_type="fts", fts_columns="text").to_arrow() + + +def test_lsm_read_time_travel_errors(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table("t", _reader([1, 2, 3])) + table.set_unenforced_primary_key("id") + table.set_lsm_write_spec(LsmWriteSpec.unsharded()) + pinned = table.version + table.add(_reader([4, 5])) # standard add commits a newer version + table.checkout(pinned) # detached head at the historical version + + # The WAL/manifest expose current live state, so an LSM read at a pinned + # historical version is rejected. + with pytest.raises(Exception, match="time-travel"): + table.search().to_arrow() + # use_lsm(False) reads the base table at the pinned version. + base = table.search().use_lsm(False).to_arrow() + assert sorted(base["id"].to_pylist()) == [1, 2, 3] + + +def test_lsm_read_take_row_ids_errors(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table("t", _reader([1, 2, 3])) + table.set_unenforced_primary_key("id") + table.set_lsm_write_spec(LsmWriteSpec.unsharded()) + _lsm_upsert(table, [4]) + # take-by-row-id auto-routes through the LSM scanner, which has no stable _rowid, + # so it hard-errors instead of failing with an opaque column-not-found error. + with pytest.raises(Exception, match="row id"): + table.take_row_ids([0, 1]).to_arrow() + # use_lsm(False) is the escape hatch: it reads the base table. + base = table.take_row_ids([0, 1]).use_lsm(False).to_arrow() + assert base.num_rows == 2 + + +def test_lsm_read_fts_postfilter_errors(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table("t", _fts_reader([(1, "quick fox"), (2, "lazy dog")])) + table.set_unenforced_primary_key("id") + table.create_index("text", config=FTS()) + index_name = table.list_indices()[0].name + table.set_lsm_write_spec( + LsmWriteSpec.unsharded().with_maintained_indexes([index_name]) + ) + # The LSM scanner always prefilters; postfilter on FTS changes result semantics, + # so it hard-errors (previously only the vector arm rejected it). + with pytest.raises(Exception, match="postfilter"): + ( + table.search("fox", query_type="fts", fts_columns="text") + .where("id > 0") + .postfilter() + .to_arrow() + ) + + +def test_lsm_read_fts_multiple_same_type_indexes_errors(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table("t", _fts_reader([(1, "quick fox"), (2, "lazy dog")])) + table.set_unenforced_primary_key("id") + table.create_index("text", config=FTS(), name="fts_a") + table.create_index("text", config=FTS(), name="fts_b", replace=False) + table.set_lsm_write_spec( + LsmWriteSpec.unsharded().with_maintained_indexes(["fts_a"]) + ) + # Two FTS indexes on the column: the base planner's chosen index is ambiguous, so + # the scanner can't pick a catch-up watermark and rejects rather than risk + # dropping rows the actually-used index has not caught up to. + with pytest.raises(Exception, match="multiple"): + table.search("fox", query_type="fts", fts_columns="text").to_arrow() + + +def test_lsm_read_vector_unmaintained_index_errors(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + rows = [ + (i, "a", [float((i * 7 + j) % 13) for j in range(VECTOR_DIM)]) + for i in range(1, 61) + ] + table = db.create_table("t", _vector_reader(rows)) + table.set_unenforced_primary_key("id") + table.create_index( + "vector", config=IvfPq(num_partitions=1, num_sub_vectors=2, num_bits=4) + ) + # Spec with NO maintained indexes: the base vector index's catch-up is untracked, + # so the scanner rejects rather than risk dropping compacted-but-unindexed rows. + table.set_lsm_write_spec(LsmWriteSpec.unsharded()) + with pytest.raises(Exception, match="maintained"): + table.search([1.0] * VECTOR_DIM).to_arrow() + + +def test_lsm_read_fts_optimized_index_not_rejected(tmp_path): + db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(seconds=0)) + table = db.create_table("t", _fts_reader([(i, "quick fox") for i in range(1, 6)])) + table.set_unenforced_primary_key("id") + table.create_index("text", config=FTS()) + table.add(_fts_reader([(i, "lazy fox") for i in range(6, 11)])) + table.optimize() # may split the FTS index into multiple physical segments + name = table.list_indices()[0].name + table.set_lsm_write_spec(LsmWriteSpec.unsharded().with_maintained_indexes([name])) + # Multiple physical segments of one logical index must not be miscounted as + # multiple indexes and rejected. + ids = set( + table.search("fox", query_type="fts", fts_columns="text") + .limit(20) + .to_arrow()["id"] + .to_pylist() + ) + assert ids == set(range(1, 11)) diff --git a/python/src/query.rs b/python/src/query.rs index 70ef3b281..affbdf4fd 100644 --- a/python/src/query.rs +++ b/python/src/query.rs @@ -294,6 +294,7 @@ pub struct PyQueryRequest { pub select: PySelect, pub fast_search: Option, pub with_row_id: Option, + pub use_lsm: Option, pub column: Option, pub query_vector: Option, pub minimum_nprobes: Option, @@ -324,6 +325,7 @@ impl From for PyQueryRequest { select: PySelect(query_request.select), fast_search: Some(query_request.fast_search), with_row_id: Some(query_request.with_row_id), + use_lsm: query_request.use_lsm, column: None, query_vector: None, minimum_nprobes: None, @@ -348,6 +350,7 @@ impl From for PyQueryRequest { select: PySelect(vector_query.base.select), fast_search: Some(vector_query.base.fast_search), with_row_id: Some(vector_query.base.with_row_id), + use_lsm: vector_query.base.use_lsm, column: vector_query.column, query_vector: Some(PyQueryVectors(vector_query.query_vector)), minimum_nprobes: Some(vector_query.minimum_nprobes), @@ -474,6 +477,10 @@ impl Query { self.inner = self.inner.clone().fast_search(); } + pub fn use_lsm(&mut self, enable: bool) { + self.inner = self.inner.clone().use_lsm(enable); + } + pub fn with_row_id(&mut self) { self.inner = self.inner.clone().with_row_id(); } @@ -636,6 +643,10 @@ impl TakeQuery { self.inner = self.inner.clone().with_row_id(); } + pub fn use_lsm(&mut self, enable: bool) { + self.inner = self.inner.clone().use_lsm(enable); + } + #[pyo3(signature = ())] pub fn output_schema(self_: PyRef<'_, Self>) -> PyResult> { let inner = self_.inner.clone(); @@ -745,6 +756,10 @@ impl FTSQuery { self.inner = self.inner.clone().fast_search(); } + pub fn use_lsm(&mut self, enable: bool) { + self.inner = self.inner.clone().use_lsm(enable); + } + pub fn with_row_id(&mut self) { self.inner = self.inner.clone().with_row_id(); } @@ -892,6 +907,10 @@ impl VectorQuery { self.inner = self.inner.clone().fast_search(); } + pub fn use_lsm(&mut self, enable: bool) { + self.inner = self.inner.clone().use_lsm(enable); + } + pub fn with_row_id(&mut self) { self.inner = self.inner.clone().with_row_id(); } @@ -1086,6 +1105,11 @@ impl HybridQuery { self.inner_fts.postfilter(); } + pub fn use_lsm(&mut self, enable: bool) { + self.inner_vec.use_lsm(enable); + self.inner_fts.use_lsm(enable); + } + pub fn add_query_vector(&mut self, vector: Bound<'_, PyAny>) -> PyResult<()> { self.inner_vec.add_query_vector(vector) } diff --git a/python/src/table.rs b/python/src/table.rs index 5d6910685..b62aa7377 100644 --- a/python/src/table.rs +++ b/python/src/table.rs @@ -1215,8 +1215,8 @@ impl Table { if let Some(use_index) = parameters.use_index { builder.use_index(use_index); } - if let Some(use_lsm_write) = parameters.use_lsm_write { - builder.use_lsm_write(use_lsm_write); + if let Some(use_lsm) = parameters.use_lsm { + builder.use_lsm(use_lsm); } if let Some(validate_single_shard) = parameters.validate_single_shard { builder.validate_single_shard(validate_single_shard); @@ -1454,7 +1454,7 @@ pub struct MergeInsertParams { when_not_matched_by_source_condition_expr: Option, timeout: Option, use_index: Option, - use_lsm_write: Option, + use_lsm: Option, validate_single_shard: Option, } diff --git a/rust/lancedb/src/lib.rs b/rust/lancedb/src/lib.rs index 0b2962682..35a564d41 100644 --- a/rust/lancedb/src/lib.rs +++ b/rust/lancedb/src/lib.rs @@ -167,6 +167,11 @@ //! # } //! ``` +// The MemWAL LSM read path (`table::query::lsm`) deepens the `create_plan` future's +// type graph enough to overflow the default trait-recursion limit while evaluating +// auto-traits (`Send`) through the Linux io_uring build's moka cache. Raise it. +#![recursion_limit = "256"] + pub mod arrow; pub mod blob; pub mod connection; diff --git a/rust/lancedb/src/query.rs b/rust/lancedb/src/query.rs index 727d87c38..b76865043 100644 --- a/rust/lancedb/src/query.rs +++ b/rust/lancedb/src/query.rs @@ -523,6 +523,26 @@ pub trait QueryBase { /// /// This allows ordering query results by one or more columns in either ascending or descending order. fn order_by(self, ordering: Option>) -> Self; + + /// Control MemWAL read routing for this query. + /// + /// By default (unset), when the table carries a MemWAL write spec (see + /// [`crate::Table::set_lsm_write_spec`]), reads are routed through the LSM + /// scanner so they also return data written via the `merge_insert` LSM path + /// that has not yet been compacted into the base table (active/frozen + /// memtables and flushed generations); a table without a spec reads the base + /// table. + /// + /// - `use_lsm(true)` forces LSM routing and errors if the table has no + /// MemWAL write spec. + /// - `use_lsm(false)` bypasses the MemWAL and reads the base table only, + /// even when a spec is present. + /// + /// Note: the LSM scanner does not support every query shape (e.g. reranking, + /// hybrid search, `order_by`). On a MemWAL table those shapes error unless + /// `use_lsm(false)` is set, because a base-only read would silently + /// exclude un-compacted MemWAL data. + fn use_lsm(self, enable: bool) -> Self; } pub trait HasQuery { @@ -593,6 +613,11 @@ impl QueryBase for T { self.mut_query().order_by = ordering; self } + + fn use_lsm(mut self, enable: bool) -> Self { + self.mut_query().use_lsm = Some(enable); + self + } } /// Options for controlling the execution of a query @@ -844,6 +869,20 @@ pub struct QueryRequest { /// /// This allows ordering query results by one or more columns in either ascending or descending order. pub order_by: Option>, + + /// Controls MemWAL read routing. When unset (the default), a query against a + /// table that carries a MemWAL write spec (see + /// [`crate::Table::set_lsm_write_spec`]) is routed through the LSM scanner so + /// it also sees data written via the `merge_insert` LSM path that has not yet + /// been compacted into the base table — the active and frozen in-memory + /// memtables and the flushed (L0) generations, deduplicated by primary key + /// against the base table (newest generation wins); a table without a spec + /// reads the base table. + /// + /// - `Some(true)` forces LSM routing and errors if the table has no MemWAL + /// write spec. + /// - `Some(false)` reads only the base table, bypassing the MemWAL. + pub use_lsm: Option, } impl Default for QueryRequest { @@ -862,6 +901,7 @@ impl Default for QueryRequest { norm: None, disable_scoring_autoprojection: false, order_by: None, + use_lsm: None, } } } diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index 92c4ad296..b17e6c2bb 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -617,6 +617,11 @@ impl RemoteTable { ) -> Result<()> { params.check_filter()?; body["prefilter"] = params.prefilter.into(); + // Only forward use_lsm when explicitly set; a server that predates it + // ignores the field and routes as it would by default. + if let Some(use_lsm) = params.use_lsm { + body["use_lsm"] = serde_json::Value::Bool(use_lsm); + } if let Some(offset) = params.offset { body["offset"] = serde_json::Value::Number(serde_json::Number::from(offset)); } @@ -2843,6 +2848,10 @@ struct MergeInsertRequest { // (the default is true) #[serde(skip_serializing_if = "is_true")] use_index: bool, + // Only serialize use_lsm when explicitly set (Some); a server that predates + // it ignores the field and routes as it would by default. + #[serde(skip_serializing_if = "Option::is_none")] + use_lsm: Option, } fn is_true(b: &bool) -> bool { @@ -2894,6 +2903,7 @@ impl TryFrom for MergeInsertRequest { when_not_matched_by_source_delete_filt, // Only serialize use_index when it's false for backwards compatibility use_index: value.use_index, + use_lsm: value.use_lsm, }) } } diff --git a/rust/lancedb/src/table/merge.rs b/rust/lancedb/src/table/merge.rs index a122ba2f2..13a633c67 100644 --- a/rust/lancedb/src/table/merge.rs +++ b/rust/lancedb/src/table/merge.rs @@ -73,7 +73,7 @@ pub struct MergeInsertBuilder { pub(crate) when_not_matched_by_source_delete_filt: Option, pub(crate) timeout: Option, pub(crate) use_index: bool, - pub(crate) use_lsm_write: Option, + pub(crate) use_lsm: Option, pub(crate) validate_single_shard: bool, } @@ -89,7 +89,7 @@ impl MergeInsertBuilder { when_not_matched_by_source_delete_filt: None, timeout: None, use_index: true, - use_lsm_write: None, + use_lsm: None, validate_single_shard: true, } } @@ -187,16 +187,17 @@ impl MergeInsertBuilder { self } - /// Controls whether `merge_insert` uses the MemWAL LSM write path. + /// Control MemWAL routing for this `merge_insert`. /// /// By default (unset), a `merge_insert` on a table with an - /// [`LsmWriteSpec`](super::LsmWriteSpec) installed is routed through - /// Lance's MemWAL shard writer, and a table without one uses the standard - /// path. Calling this with `false` forces the standard path even when a - /// spec is set. Calling it with `true` requires a spec — `merge_insert` - /// errors if none is installed. - pub fn use_lsm_write(&mut self, use_lsm_write: bool) -> &mut Self { - self.use_lsm_write = Some(use_lsm_write); + /// [`LsmWriteSpec`](super::LsmWriteSpec) installed is routed through Lance's + /// MemWAL shard writer; a table without one uses the standard path. + /// + /// - `use_lsm(true)` forces MemWAL routing and errors if the table has no + /// LSM write spec. + /// - `use_lsm(false)` forces the standard write path even when a spec is set. + pub fn use_lsm(&mut self, enable: bool) -> &mut Self { + self.use_lsm = Some(enable); self } @@ -626,7 +627,7 @@ mod lsm_tests { } #[tokio::test] - async fn lsm_merge_insert_use_lsm_write_false_falls_back() { + async fn lsm_merge_insert_use_lsm_false_falls_back() { let dir = tempdir().unwrap(); let table = id_value_table(&dir).await; table @@ -634,9 +635,10 @@ mod lsm_tests { .await .unwrap(); - // use_lsm_write(false) opts out: the standard path runs and commits. + // use_lsm(false) opts out: the standard path runs and commits even though + // a spec is installed. let mut builder = table.merge_insert(&["id"]); - builder.when_not_matched_insert_all().use_lsm_write(false); + builder.when_not_matched_insert_all().use_lsm(false); let result = builder .execute(id_value_reader(vec![3, 4, 5])) .await @@ -646,6 +648,25 @@ mod lsm_tests { assert_eq!(table.count_rows(None).await.unwrap(), 5); } + #[tokio::test] + async fn lsm_merge_insert_use_lsm_true_without_spec_errors() { + let dir = tempdir().unwrap(); + let table = id_value_table(&dir).await; + + // use_lsm(true) demands MemWAL routing; without a write spec it errors + // rather than silently falling back to the standard path. + let mut builder = table.merge_insert(&["id"]); + builder + .when_matched_update_all(None) + .when_not_matched_insert_all() + .use_lsm(true); + let err = builder + .execute(id_value_reader(vec![3, 4, 5])) + .await + .unwrap_err(); + assert!(matches!(err, Error::InvalidInput { .. }), "got {err:?}"); + } + #[tokio::test] async fn lsm_merge_insert_rejects_on_not_primary_key() { let dir = tempdir().unwrap(); @@ -754,18 +775,23 @@ mod lsm_tests { } #[tokio::test] - async fn lsm_merge_insert_use_lsm_write_true_requires_spec() { + async fn lsm_merge_insert_no_spec_uses_standard_path() { let dir = tempdir().unwrap(); // id_value_table sets a primary key but no LSM write spec. let table = id_value_table(&dir).await; + // Without a spec, a default merge_insert (use_lsm unset) simply uses + // the standard path and commits — no opt-out required, no error. let mut builder = table.merge_insert(&["id"]); builder .when_matched_update_all(None) - .when_not_matched_insert_all() - .use_lsm_write(true); - let err = builder.execute(id_value_reader(vec![4])).await.unwrap_err(); - assert!(matches!(err, Error::InvalidInput { .. }), "got {err:?}"); + .when_not_matched_insert_all(); + let result = builder + .execute(id_value_reader(vec![3, 4, 5])) + .await + .unwrap(); + assert_eq!(result.num_inserted_rows, 2); + assert_eq!(table.count_rows(None).await.unwrap(), 5); } #[tokio::test] @@ -818,4 +844,387 @@ mod lsm_tests { .await .unwrap(); } + + // --------------------------------------------------------------------- + // LSM read path + // --------------------------------------------------------------------- + + use crate::arrow::SendableRecordBatchStream; + use crate::query::{ExecutableQuery, QueryBase}; + use arrow::array::AsArray; + use arrow::datatypes::Int64Type; + use futures::TryStreamExt; + + /// Collect `(id, value)` pairs from a result stream, sorted by id. + async fn collect_id_value(stream: SendableRecordBatchStream) -> Vec<(i64, i64)> { + let batches: Vec<_> = stream.try_collect().await.unwrap(); + let mut rows = Vec::new(); + for batch in &batches { + let ids = batch + .column_by_name("id") + .unwrap() + .as_primitive::(); + let values = batch + .column_by_name("value") + .unwrap() + .as_primitive::(); + for i in 0..batch.num_rows() { + rows.push((ids.value(i), values.value(i))); + } + } + rows.sort(); + rows + } + + /// Upsert `ids` (value = 0..n) through the LSM `merge_insert` path. + async fn lsm_upsert(table: &Table, ids: Vec) { + let mut builder = table.merge_insert(&[]); + builder + .when_matched_update_all(None) + .when_not_matched_insert_all(); + builder.execute(id_value_reader(ids)).await.unwrap(); + } + + #[tokio::test] + async fn lsm_read_sees_active_memtable() { + let dir = tempdir().unwrap(); + let table = id_value_table(&dir).await; // base: ids 1,2,3 (value 0,1,2) + table + .set_lsm_write_spec(LsmWriteSpec::unsharded()) + .await + .unwrap(); + + // Insert ids 4,5 into the active memtable (not committed to base). + lsm_upsert(&table, vec![4, 5]).await; + + // Default read auto-routes through the LSM scanner: base ∪ active memtable. + let lsm = table.query().execute().await.unwrap(); + let rows = collect_id_value(lsm).await; + assert_eq!( + rows.iter().map(|(id, _)| *id).collect::>(), + vec![1, 2, 3, 4, 5] + ); + + // use_lsm(false) bypasses the MemWAL and reads the base table only. + let base_only = table.query().use_lsm(false).execute().await.unwrap(); + let rows = collect_id_value(base_only).await; + assert_eq!( + rows.iter().map(|(id, _)| *id).collect::>(), + vec![1, 2, 3] + ); + } + + #[tokio::test] + async fn lsm_read_dedup_newest_wins() { + let dir = tempdir().unwrap(); + let table = id_value_table(&dir).await; // base: id 2 -> value 1 + table + .set_lsm_write_spec(LsmWriteSpec::unsharded()) + .await + .unwrap(); + + // Upsert ids 2,3,4 with values 0,1,2. id 2 and 3 shadow the base rows. + lsm_upsert(&table, vec![2, 3, 4]).await; + + let lsm = table.query().execute().await.unwrap(); + let rows = collect_id_value(lsm).await; + // id 1 from base (value 0); ids 2,3,4 from memtable (values 0,1,2). + assert_eq!(rows, vec![(1, 0), (2, 0), (3, 1), (4, 2)]); + } + + #[tokio::test] + async fn lsm_read_point_lookup_filter() { + let dir = tempdir().unwrap(); + let table = id_value_table(&dir).await; + table + .set_lsm_write_spec(LsmWriteSpec::unsharded()) + .await + .unwrap(); + lsm_upsert(&table, vec![2, 3, 4]).await; // id 2 -> value 0 (shadows base) + + let lsm = table.query().only_if("id = 2").execute().await.unwrap(); + let rows = collect_id_value(lsm).await; + assert_eq!(rows, vec![(2, 0)]); + } + + #[tokio::test] + async fn lsm_read_multi_shard() { + let dir = tempdir().unwrap(); + let table = id_value_table(&dir).await; + table + .set_lsm_write_spec(LsmWriteSpec::bucket("id", 8)) + .await + .unwrap(); + + // Two single-row upserts that route to (likely) different buckets; each + // closes the writer so the next opens a fresh shard. + lsm_upsert(&table, vec![10]).await; + table.close_lsm_writers().await.unwrap(); + lsm_upsert(&table, vec![11]).await; + + let lsm = table.query().execute().await.unwrap(); + let rows = collect_id_value(lsm).await; + let ids: Vec = rows.iter().map(|(id, _)| *id).collect(); + // Base 1,2,3 + flushed/active shards for 10 and 11. + assert_eq!(ids, vec![1, 2, 3, 10, 11]); + } + + #[tokio::test] + async fn lsm_read_after_close_sees_flushed() { + let dir = tempdir().unwrap(); + let table = id_value_table(&dir).await; + table + .set_lsm_write_spec(LsmWriteSpec::unsharded()) + .await + .unwrap(); + lsm_upsert(&table, vec![4, 5]).await; + // close flushes the active memtable to an on-disk generation and drops + // the cached writer; the read must still see those rows via the shard + // manifest snapshot. + table.close_lsm_writers().await.unwrap(); + + let lsm = table.query().execute().await.unwrap(); + let ids: Vec = collect_id_value(lsm) + .await + .iter() + .map(|(id, _)| *id) + .collect(); + assert_eq!(ids, vec![1, 2, 3, 4, 5]); + } + + #[tokio::test] + async fn lsm_read_without_spec_reads_base() { + let dir = tempdir().unwrap(); + let table = id_value_table(&dir).await; // no LSM write spec + + // With no spec installed there is nothing to route: the default read and + // an explicit use_lsm(false) both read the base table without error. + for query in [table.query(), table.query().use_lsm(false)] { + let rows = collect_id_value(query.execute().await.unwrap()).await; + assert_eq!( + rows.iter().map(|(id, _)| *id).collect::>(), + vec![1, 2, 3] + ); + } + } + + #[tokio::test] + async fn lsm_read_unsupported_shape_errors_without_use_lsm_false() { + let dir = tempdir().unwrap(); + let table = id_value_table(&dir).await; + table + .set_lsm_write_spec(LsmWriteSpec::unsharded()) + .await + .unwrap(); + lsm_upsert(&table, vec![4]).await; + + // `with_row_id` is a shape the LSM scanner cannot honor. On a MemWAL + // table the default (auto-routed) read hard-errors rather than silently + // reading a stale base-only result that would exclude un-compacted row 4. + let err = table + .query() + .with_row_id() + .execute() + .await + .err() + .expect("unsupported shape on a MemWAL table must error"); + assert!(matches!(err, Error::NotSupported { .. }), "got {err:?}"); + + // use_lsm(false) is the escape hatch: it reads the base table only. + let rows = collect_id_value( + table + .query() + .with_row_id() + .use_lsm(false) + .execute() + .await + .unwrap(), + ) + .await; + assert_eq!( + rows.iter().map(|(id, _)| *id).collect::>(), + vec![1, 2, 3] + ); + } + + /// A reader of `[id: Int64, text: Utf8]` rows. + fn id_text_reader(rows: Vec<(i64, &str)>) -> Box { + let schema = Arc::new(Schema::new(vec![ + Field::new("id", DataType::Int64, false), + Field::new("text", DataType::Utf8, false), + ])); + let ids: Vec = rows.iter().map(|(id, _)| *id).collect(); + let texts: Vec<&str> = rows.iter().map(|(_, t)| *t).collect(); + let batch = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(Int64Array::from(ids)), + Arc::new(StringArray::from(texts)), + ], + ) + .unwrap(); + Box::new(RecordBatchIterator::new(vec![Ok(batch)], schema)) + } + + #[tokio::test] + async fn lsm_read_full_text_search() { + use crate::index::Index; + use lance_index::scalar::FullTextSearchQuery; + + let dir = tempdir().unwrap(); + let conn = connect(dir.path().to_str().unwrap()) + .execute() + .await + .unwrap(); + let table = conn + .create_table( + "t", + id_text_reader(vec![(1, "alpha"), (2, "beta"), (3, "gamma")]), + ) + .execute() + .await + .unwrap(); + table.set_unenforced_primary_key(["id"]).await.unwrap(); + table + .create_index(&["text"], Index::FTS(Default::default())) + .execute() + .await + .unwrap(); + let fts_index = table.list_indices().await.unwrap()[0].name.clone(); + table + .set_lsm_write_spec(LsmWriteSpec::unsharded().with_maintained_indexes([fts_index])) + .await + .unwrap(); + + // Insert a row whose term ("zebra") exists in no base row. + let mut builder = table.merge_insert(&[]); + builder + .when_matched_update_all(None) + .when_not_matched_insert_all(); + builder + .execute(id_text_reader(vec![(99, "zebra")])) + .await + .unwrap(); + + let search = |term: &str| { + let q = FullTextSearchQuery::new(term.to_string()) + .with_column("text".to_string()) + .unwrap(); + table.query().full_text_search(q) + }; + + // "zebra" lives only in the active memtable; LSM read finds it. + let stream = search("zebra").execute().await.unwrap(); + let batches: Vec<_> = stream.try_collect().await.unwrap(); + let rows: usize = batches.iter().map(|b| b.num_rows()).sum(); + assert_eq!(rows, 1, "LSM FTS must surface the memtable row"); + + // A base-only term still matches the base table through the LSM scan. + let stream = search("alpha").execute().await.unwrap(); + let batches: Vec<_> = stream.try_collect().await.unwrap(); + let rows: usize = batches.iter().map(|b| b.num_rows()).sum(); + assert_eq!(rows, 1, "LSM FTS must still see base rows"); + } + + #[tokio::test] + async fn lsm_read_vector_search() { + use crate::index::Index; + use crate::index::vector::IvfPqIndexBuilder; + use arrow::array::{FixedSizeListBuilder, Float32Builder}; + use arrow::datatypes::Int64Type; + + const DIM: i32 = 8; + const N: i64 = 256; + + let schema = Arc::new(Schema::new(vec![ + Field::new("id", DataType::Int64, false), + Field::new( + "vec", + DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float32, true)), DIM), + false, + ), + ])); + let make_batch = |rows: Vec<(i64, f32)>| -> RecordBatch { + let ids: Vec = rows.iter().map(|(id, _)| *id).collect(); + let mut vb = FixedSizeListBuilder::new(Float32Builder::new(), DIM); + for (_, fill) in &rows { + for _ in 0..DIM { + vb.values().append_value(*fill); + } + vb.append(true); + } + RecordBatch::try_new( + schema.clone(), + vec![Arc::new(Int64Array::from(ids)), Arc::new(vb.finish())], + ) + .unwrap() + }; + + let dir = tempdir().unwrap(); + let conn = connect(dir.path().to_str().unwrap()) + .execute() + .await + .unwrap(); + // Base rows fill each vector with its own id (0..256); all far from 1000. + let base = make_batch((0..N).map(|i| (i, i as f32)).collect()); + let base_reader: Box = + Box::new(RecordBatchIterator::new(vec![Ok(base)], schema.clone())); + let table = conn.create_table("t", base_reader).execute().await.unwrap(); + table.set_unenforced_primary_key(["id"]).await.unwrap(); + table + .create_index( + &["vec"], + Index::IvfPq( + IvfPqIndexBuilder::default() + .num_partitions(1) + .num_sub_vectors(2), + ), + ) + .execute() + .await + .unwrap(); + let vec_index = table.list_indices().await.unwrap()[0].name.clone(); + table + .set_lsm_write_spec(LsmWriteSpec::unsharded().with_maintained_indexes([vec_index])) + .await + .unwrap(); + + // Insert a vector (filled with 1000) that is nearest to the query. + let mut builder = table.merge_insert(&[]); + builder + .when_matched_update_all(None) + .when_not_matched_insert_all(); + let insert_reader: Box = Box::new(RecordBatchIterator::new( + vec![Ok(make_batch(vec![(9999, 1000.0)]))], + schema.clone(), + )); + builder.execute(insert_reader).await.unwrap(); + + // KNN near [1000; DIM]: the default (auto-routed) read surfaces the + // memtable row. + let stream = table + .query() + .nearest_to(&[1000.0_f32; 8]) + .unwrap() + .limit(1) + .execute() + .await + .unwrap(); + let batches: Vec<_> = stream.try_collect().await.unwrap(); + let ids: Vec = batches + .iter() + .flat_map(|b| { + b.column_by_name("id") + .unwrap() + .as_primitive::() + .values() + .to_vec() + }) + .collect(); + assert_eq!( + ids, + vec![9999], + "LSM vector search must rank the memtable row first" + ); + } } diff --git a/rust/lancedb/src/table/merge/lsm.rs b/rust/lancedb/src/table/merge/lsm.rs index 9bc6ad088..0eb7c0231 100644 --- a/rust/lancedb/src/table/merge/lsm.rs +++ b/rust/lancedb/src/table/merge/lsm.rs @@ -306,6 +306,39 @@ impl ShardWriterEntry { } Ok(()) } + + /// The cached writer's latest in-memory manifest (current generation + + /// flushed generations). `Ok(None)` if the writer was already closed. + /// Used by the LSM read path to snapshot this shard authoritatively + /// without re-reading the on-disk manifest. + async fn manifest(&self) -> Result> { + let guard = self.inner.read().await; + let Some(writer) = guard.as_ref() else { + return Ok(None); + }; + writer.manifest().await.map_err(|e| Error::Runtime { + message: format!("read: shard writer manifest read failed: {}", e), + }) + } + + /// Atomically capture the cached writer's active + frozen-awaiting-flush + /// memtables for unified LSM scanning. `Ok(None)` if the writer was + /// already closed. + async fn in_memory_memtable_refs( + &self, + ) -> Result> { + let guard = self.inner.read().await; + let Some(writer) = guard.as_ref() else { + return Ok(None); + }; + writer + .in_memory_memtable_refs() + .await + .map(Some) + .map_err(|e| Error::Runtime { + message: format!("read: shard writer memtable capture failed: {}", e), + }) + } } impl ShardWriterCache { @@ -345,6 +378,36 @@ impl ShardWriterCache { Ok(entry) } + /// Snapshot the cached writer's shard for the LSM read path: its shard id, + /// authoritative in-memory manifest, and active + frozen memtable refs. + /// Returns `None` when no writer is currently cached (e.g. nothing has been + /// written this session, or the writer was closed). + #[allow(clippy::redundant_pub_crate)] + pub(crate) async fn read_snapshot( + &self, + ) -> Result< + Option<( + Uuid, + Option, + Option, + )>, + > { + let cached = { + let guard = self.slot.read().await; + guard.as_ref().map(|(id, entry)| (*id, entry.clone())) + }; + let Some((shard_id, entry)) = cached else { + return Ok(None); + }; + // Capture memtables before the manifest. If a flush interleaves, dedup + // tolerates the same rows appearing in both a memtable and a freshly + // flushed generation, but would drop rows present in neither. Manifest + // last guarantees any generation flushed mid-capture is still covered. + let memtables = entry.in_memory_memtable_refs().await?; + let manifest = entry.manifest().await?; + Ok(Some((shard_id, manifest, memtables))) + } + /// Close the cached writer, if any, and clear the slot. #[allow(clippy::redundant_pub_crate)] pub(crate) async fn drain_and_close(&self) -> Result<()> { @@ -408,19 +471,20 @@ pub(crate) async fn lsm_dispatch_decision( table: &NativeTable, params: &MergeInsertBuilder, ) -> Result { - // `Some(false)` is an explicit opt-out: use the standard path. - if params.use_lsm_write == Some(false) { + // Explicit opt-out: use the standard path regardless of any installed spec. + if params.use_lsm == Some(false) { return Ok(LsmDispatch::Standard); } let dataset = table.dataset.get().await?; let Some(details) = dataset.mem_wal_index_details().await? else { - // No LSM write spec installed. `Some(true)` explicitly asked for the - // LSM path, which is meaningless without a spec; `None` (the default) - // just falls back to the standard path. - if params.use_lsm_write == Some(true) { + // No write spec installed. `use_lsm(true)` demanded MemWAL routing, so + // that is an error; otherwise fall back to the standard path. + if params.use_lsm == Some(true) { return Err(Error::InvalidInput { - message: "merge_insert: use_lsm_write(true) requires an LSM write spec on the table; call set_lsm_write_spec first".to_string(), + message: "use_lsm(true) was set but the table has no MemWAL write spec; \ + install one with set_lsm_write_spec or leave use_lsm unset" + .to_string(), }); } return Ok(LsmDispatch::Standard); @@ -449,7 +513,7 @@ pub(crate) async fn lsm_dispatch_decision( if !is_upsert_only(params) { return Err(Error::InvalidInput { - message: "merge_insert: when an LSM write spec is set, only the upsert form (when_matched_update_all without a filter + when_not_matched_insert_all, no by-source delete) is supported; call use_lsm_write(false) to use the standard merge_insert path".to_string(), + message: "merge_insert: when an LSM write spec is set, only the upsert form (when_matched_update_all without a filter + when_not_matched_insert_all, no by-source delete) is supported; call use_lsm(false) to use the standard merge_insert path".to_string(), }); } diff --git a/rust/lancedb/src/table/query.rs b/rust/lancedb/src/table/query.rs index 28845e96f..9feb9d5ab 100644 --- a/rust/lancedb/src/table/query.rs +++ b/rust/lancedb/src/table/query.rs @@ -3,6 +3,8 @@ use std::sync::Arc; +mod lsm; + use super::NativeTable; use crate::connection::NamespaceClientPushdownOperation; use crate::error::{Error, Result}; @@ -20,6 +22,7 @@ use datafusion_physical_plan::projection::ProjectionExec; use datafusion_physical_plan::repartition::RepartitionExec; use datafusion_physical_plan::union::UnionExec; use futures::future::try_join_all; +use lance::dataset::mem_wal::DatasetMemWalExt; use lance::dataset::scanner::DatasetRecordBatchStream; use lance::dataset::scanner::Scanner; use lance_datafusion::exec::{analyze_plan as lance_analyze_plan, execute_plan}; @@ -53,7 +56,7 @@ pub async fn execute_query( // QueryTable pushdown runs the query server-side, but only on the main // branch: the namespace request carries no branch yet, so a branch handle // must fall through to local execution. - if can_execute_namespace_query(table, query) + if can_execute_namespace_query(table, query).await? && let Some(ref namespace_client) = table.namespace_client { return execute_namespace_query(table, namespace_client.clone(), query, options).await; @@ -61,18 +64,35 @@ pub async fn execute_query( execute_generic_query(table, query, options).await } -fn can_execute_namespace_query(table: &NativeTable, query: &AnyQuery) -> bool { - table +async fn can_execute_namespace_query(table: &NativeTable, query: &AnyQuery) -> Result { + if !(table .pushdown_operations .contains(&NamespaceClientPushdownOperation::QueryTable) && table.namespace_client.is_some() && table.dataset.current_branch().is_none() - && !requires_local_namespace_execution(query) + && !requires_local_namespace_execution(query)) + { + return Ok(false); + } + // A MemWAL write spec means reads auto-route through the LSM scanner in + // `create_plan` even when `use_lsm` is unset. The namespace request has no + // use_lsm field, so pushing the default query down would silently omit + // un-compacted rows — force local execution whenever a spec is installed. + let dataset = table.dataset.get().await?; + if dataset.mem_wal_index_details().await?.is_some() { + return Ok(false); + } + Ok(true) } fn requires_local_namespace_execution(query: &AnyQuery) -> bool { - // The namespace QueryTable request has no approx_mode field yet, so - // pushing this query down would silently ignore the user's setting. + // The namespace QueryTable request has no approx_mode or use_lsm field yet, so + // pushing these down would silently ignore the user's setting. For use_lsm that + // is worse than a tuning miss: MemWAL read routing lives only in `create_plan`, + // so a pushed-down query would return stale base-only data with no error. + if query.base().use_lsm.is_some() { + return true; + } matches!( query, AnyQuery::VectorQuery(VectorQueryRequest { @@ -120,6 +140,29 @@ pub async fn create_plan( query.base.check_filter()?; let ds_ref = table.dataset.get().await?; + + // MemWAL read routing driven by `use_lsm`: + // * unset — route through the LSM scanner iff the table carries a write spec + // * Some(true) — force LSM routing; error if the table has no write spec + // * Some(false) — read the base table only, bypassing the MemWAL + // The LSM scanner surfaces in-flight `merge_insert` data (active/frozen + // memtables + flushed generations); validation and dispatch live in `lsm`. + let has_spec = ds_ref.mem_wal_index_details().await?.is_some(); + let use_lsm = match query.base.use_lsm { + Some(true) if !has_spec => { + return Err(Error::InvalidInput { + message: "use_lsm(true) was set but the table has no MemWAL write spec; \ + install one with set_lsm_write_spec or leave use_lsm unset" + .to_string(), + }); + } + Some(enable) => enable, + None => has_spec, + }; + if use_lsm { + return lsm::create_lsm_plan(table, ds_ref, query).await; + } + let schema = ds_ref.schema(); let mut column = query.column.clone(); @@ -904,6 +947,65 @@ mod tests { assert_eq!(namespace_client.query_table_calls.load(Ordering::SeqCst), 0); } + #[tokio::test] + async fn test_execute_query_use_lsm_with_namespace_pushdown_runs_locally() { + use crate::connect; + use crate::table::query::execute_query; + use arrow_array::{Int32Array, RecordBatch}; + use arrow_schema::{DataType, Field, Schema}; + + let conn = connect("memory://").execute().await.unwrap(); + + let vectors = Arc::new(fixed_size_list_array( + vec![0.0, 0.0, 10.0, 10.0, 20.0, 20.0], + 2, + )); + let schema = Arc::new(Schema::new(vec![ + Field::new("id", DataType::Int32, false), + Field::new("vector", vectors.data_type().clone(), false), + ])); + let batch = RecordBatch::try_new( + schema, + vec![Arc::new(Int32Array::from(vec![1, 2, 3])), vectors], + ) + .unwrap(); + + let table = conn + .create_table("test_use_lsm_namespace_fallback", batch) + .execute() + .await + .unwrap(); + let namespace_client = Arc::new(CountingNamespaceClient::default()); + let mut native_table = table.as_native().unwrap().clone(); + native_table.namespace_client = Some(namespace_client.clone()); + native_table + .pushdown_operations + .insert(NamespaceClientPushdownOperation::QueryTable); + + // `use_lsm` set (even to false) must force local execution — the namespace + // request has no use_lsm field, so a pushdown would silently ignore it. + let query_vector = Arc::new(Float32Array::from(vec![0.0, 0.0])); + let query = AnyQuery::VectorQuery(VectorQueryRequest { + base: QueryRequest { + limit: Some(1), + use_lsm: Some(false), + ..Default::default() + }, + column: Some("vector".to_string()), + query_vector: vec![query_vector as ArrayRef], + ..Default::default() + }); + + let stream = execute_query(&native_table, &query, QueryExecutionOptions::default()) + .await + .unwrap(); + let batches = stream.try_collect::>().await.unwrap(); + let count: usize = batches.iter().map(|b| b.num_rows()).sum(); + + assert_eq!(count, 1); + assert_eq!(namespace_client.query_table_calls.load(Ordering::SeqCst), 0); + } + #[tokio::test] async fn test_create_plan_multivector_structure() { use arrow_array::{Float32Array, RecordBatch}; diff --git a/rust/lancedb/src/table/query/lsm.rs b/rust/lancedb/src/table/query/lsm.rs new file mode 100644 index 000000000..074d13476 --- /dev/null +++ b/rust/lancedb/src/table/query/lsm.rs @@ -0,0 +1,786 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The LanceDB Authors + +//! MemWAL LSM read path. +//! +//! When a table has an LSM write spec installed (see [`set_lsm_write_spec`]), +//! reads are routed through Lance's [`LsmScanner`] instead of the plain +//! base-table scan unless the query sets +//! [`use_lsm(false)`](crate::query::QueryBase::use_lsm). This makes data +//! written via the LSM `merge_insert` path — which lives in the active/frozen +//! in-memory memtables and the flushed SSTable generations until an external +//! compaction merges it into the base table — visible to queries, deduplicated by +//! primary key (newest generation wins). +//! +//! Three query shapes are supported, mirroring the standard scan: a plain scan +//! (filter / projection / limit), full-text search, and vector (ANN) search. All +//! three run through a single [`LsmScanner`], so a `where` filter is honored as a +//! prefilter uniformly — including for vector search, where `LsmScanner` threads +//! it into the vector planner's prefilter. Shapes the LSM path cannot honor are +//! rejected with [`Error::NotSupported`]; the caller must set `use_lsm(false)` to +//! run those against the base table. +//! +//! [`set_lsm_write_spec`]: crate::Table::set_lsm_write_spec + +use std::collections::HashMap; +use std::sync::Arc; + +use arrow_array::Array; +use arrow_schema::{DataType, Schema as ArrowSchema}; +use datafusion_physical_plan::expressions::Column; +use datafusion_physical_plan::projection::ProjectionExec; +use datafusion_physical_plan::{ExecutionPlan, PhysicalExpr}; +use lance::Dataset; +use lance::dataset::mem_wal::scanner::InMemoryMemTables; +use lance::dataset::mem_wal::{ + DatasetMemWalExt, LsmScanner, ShardManifestStore, ShardSnapshot, ShardWriterConfig, +}; +use lance_index::mem_wal::{MemWalIndexDetails, ShardManifest}; +use uuid::Uuid; + +use super::NativeTable; +use crate::DistanceType; +use crate::error::{Error, Result}; +use crate::query::{DEFAULT_TOP_K, QueryFilter, Select, VectorQueryRequest}; +use crate::utils::default_vector_column; + +/// Over-fetch factor for the LSM vector/FTS arms. With the default of `1.0` a +/// source blocked by cross-generation PK dedup fetches exactly `k` and can return +/// fewer than `k` live rows; upserts routinely create such blocked candidates, so +/// widen the per-source fetch to keep result pages filled. +const LSM_OVERFETCH_FACTOR: f64 = 2.0; + +/// Build the LSM read plan for a MemWAL-routed query. +/// +/// The caller guarantees `ds_ref` carries a MemWAL write spec (routing is decided +/// in [`create_plan`](super::create_plan)). Errors with [`Error::NotSupported`] +/// for query shapes the LSM scanner cannot honor — the caller must set +/// `use_lsm(false)` to run those against the base table. +pub(super) async fn create_lsm_plan( + table: &NativeTable, + ds_ref: Arc, + query: VectorQueryRequest, +) -> Result> { + reject_unsupported(&query)?; + + // A time-traveled (checked-out) handle pins an older dataset version, but the + // WAL manifests and cached writer expose current live state — mixing them would + // surface WAL rows written after the requested version. `use_lsm(false)` reads + // the base table at the pinned version. + if table.dataset.time_travel_version().is_some() { + return Err(Error::NotSupported { + message: "the MemWAL LSM scanner cannot read from a time-traveled dataset version; set use_lsm(false) to read the base table at this version".to_string(), + }); + } + + // Routing guarantees a write spec is installed (see `create_plan`). + let details = ds_ref + .mem_wal_index_details() + .await? + .ok_or_else(|| Error::Runtime { + message: "the MemWAL LSM write spec disappeared during read planning".to_string(), + })?; + + let pk_columns = pk_columns(&ds_ref)?; + // The base index an indexed arm relies on may lag compaction; resolve it so the + // snapshot retains SSTables the index has not yet caught up to. + let arm_index = arm_maintained_index_name(&ds_ref, &query, &details).await?; + let (snapshots, in_memory) = + build_read_context(table, &ds_ref, &details, arm_index.as_deref()).await?; + + let limit = query.base.limit; + let offset = query.base.offset; + + let plan = if !query.query_vector.is_empty() { + vector_plan( + &ds_ref, + &query, + &details, + pk_columns.clone(), + snapshots, + in_memory, + limit, + offset, + ) + .await? + } else if let Some(fts) = &query.base.full_text_search { + fts_plan( + &ds_ref, + fts.clone(), + &query, + &details, + pk_columns.clone(), + snapshots, + in_memory, + limit, + offset, + ) + .await? + } else { + plain_plan( + &ds_ref, + &query, + pk_columns.clone(), + snapshots, + in_memory, + limit, + offset, + ) + .await? + }; + + // Lance appends the primary-key columns internally for dedup and keeps them in + // the output; drop the ones the user did not request so the projection matches. + restore_projection(plan, &query, &pk_columns) +} + +/// Reject query shapes the LSM read path does not implement. On a MemWAL table +/// reads route through the LSM scanner by default, so an unsupported shape is a +/// hard error rather than a silent fallback to the base-only scan — which would +/// exclude un-compacted MemWAL data. The caller must set `use_lsm(false)` to +/// run these against the base table, accepting that the results omit un-compacted +/// MemWAL data. +/// +/// A `where` filter is intentionally *not* rejected: every arm routes through +/// [`LsmScanner`], which applies it as a prefilter (see [`base_scanner`]). +fn reject_unsupported(query: &VectorQueryRequest) -> Result<()> { + let unsupported = |what: &str| { + Err(Error::NotSupported { + message: format!( + "the MemWAL LSM scanner does not support {what}; set use_lsm(false) to read the base table only (results will exclude un-compacted MemWAL data)" + ), + }) + }; + if query.query_vector.len() > 1 { + return unsupported("multiple query vectors"); + } + if !query.query_vector.is_empty() && query.base.full_text_search.is_some() { + return unsupported("hybrid (vector + full-text) search"); + } + if query.base.with_row_id { + return unsupported("with_row_id (the LSM scanner exposes _rowaddr, not a stable _rowid)"); + } + if query.base.reranker.is_some() { + return unsupported("reranking / hybrid search"); + } + if query.base.order_by.is_some() { + return unsupported("order_by"); + } + // Vector-only knobs the LSM scanner cannot honor. Both change results rather + // than just recall, so error instead of silently ignoring them: distance_range + // would return rows outside the bound, and use_index(false) asks for a + // brute-force search the index-only base arm can't do. (ef / approx_mode / + // maximum_nprobes are recall/speed knobs and are left to no-op — and + // maximum_nprobes defaults to Some, so it cannot be rejected on presence.) + if !query.query_vector.is_empty() { + if query.lower_bound.is_some() || query.upper_bound.is_some() { + return unsupported("distance_range on vector search"); + } + if !query.use_index { + return unsupported( + "use_index(false) / brute-force vector search (the LSM base arm is index-only)", + ); + } + } + // Postfilter changes result semantics for both vector and full-text search, and + // the LSM scanner always prefilters — reject a requested postfilter for either. + if (!query.query_vector.is_empty() || query.base.full_text_search.is_some()) + && !query.base.prefilter + { + return unsupported( + "postfilter on vector or full-text search (the LSM scanner always prefilters)", + ); + } + match &query.base.select { + Select::All | Select::Columns(_) => {} + Select::Dynamic(_) | Select::Expr(_) => return unsupported("dynamic column projection"), + } + if let Some(QueryFilter::Substrait(_)) = &query.base.filter { + return unsupported("Substrait filters"); + } + // Take-by-row-id / row-offset queries carry a `_rowid` / `_rowoffset` filter, + // columns the LSM scanner never exposes (only `_rowaddr`); reject with guidance + // rather than failing deep in datafusion with a column-not-found error. + if let Some(QueryFilter::Datafusion(expr)) = &query.base.filter + && expr + .column_refs() + .iter() + .any(|c| c.name == "_rowid" || c.name == "_rowoffset") + { + return unsupported( + "take by row id or row offset (the LSM scanner has no stable _rowid / _rowoffset)", + ); + } + Ok(()) +} + +/// Primary-key column names from the dataset's unenforced primary key. +fn pk_columns(dataset: &Dataset) -> Result> { + let pk: Vec = dataset + .schema() + .unenforced_primary_key() + .iter() + .map(|f| f.name.clone()) + .collect(); + if pk.is_empty() { + return Err(Error::InvalidInput { + message: + "the MemWAL LSM scanner requires an unenforced primary key, but the table has none" + .to_string(), + }); + } + Ok(pk) +} + +/// Per-shard SSTable exclusion watermark: the generation at or below which SSTables +/// are safe to drop for this arm. A generation is droppable only once it is +/// compacted into the base table AND covered by `index_name`'s catch-up (for an +/// indexed arm); a plain scan (`index_name == None`) uses the compaction watermark +/// alone. Capping at the index catch-up keeps rows the base index has not yet +/// indexed visible through their SSTable. First occurrence per shard mirrors Lance's +/// `compacted_generation_for_shard`. +fn exclusion_watermarks( + details: &MemWalIndexDetails, + index_name: Option<&str>, +) -> HashMap { + let mut exclude: HashMap = HashMap::new(); + for entry in &details.compacted_sstables { + let mut watermark = entry.generation; + if let Some(name) = index_name + && let Some(caught_up) = details + .index_catchup + .iter() + .find(|icp| icp.index_name == name) + .and_then(|icp| icp.caught_up_generation_for_shard(&entry.shard_id)) + { + watermark = watermark.min(caught_up); + } + exclude.entry(entry.shard_id).or_insert(watermark); + } + exclude +} + +/// Assemble the per-shard snapshots (flushed SSTable generations) and the +/// in-memory memtables (active + frozen) for the table. +/// +/// Snapshots for all shards come from their on-disk manifests; for the shard +/// with a live cached `ShardWriter` (this session's in-flight writes) the +/// writer's authoritative in-memory manifest and memtables override the +/// on-disk view so a read sees data not yet flushed. +async fn build_read_context( + table: &NativeTable, + dataset: &Dataset, + details: &MemWalIndexDetails, + index_name: Option<&str>, +) -> Result<(Vec, HashMap)> { + let exclude = exclusion_watermarks(details, index_name); + + let shard_ids = dataset.list_mem_wal_latest_shard_ids().await?; + // Use the dataset's own object store (not `ObjectStore::from_uri`, which + // builds a fresh registry and would miss `memory://` and custom-registered + // stores). The base path matches `list_mem_wal_latest_shard_ids`. + let store = dataset.object_store(None).await?; + let base_path = dataset.branch_location().path; + let scan_batch_size = ShardWriterConfig::default().manifest_scan_batch_size; + + let mut snapshots: Vec = Vec::new(); + for shard_id in shard_ids { + let manifest_store = + ShardManifestStore::new(store.clone(), &base_path, shard_id, scan_batch_size); + if let Some(manifest) = manifest_store.read_latest().await? { + snapshots.push(snapshot_from_manifest(shard_id, &manifest, &exclude)); + } + } + + // WAL-only writers (enable_memtable=false) keep no in-memory memtable, and + // `in_memory_memtable_refs` errors in that mode; the on-disk manifests above + // already cover their flushed SSTables, so skip the live-writer snapshot. (Lance + // forbids maintained indexes in WAL-only mode, so only plain scans reach here.) + let wal_only = details + .writer_config_defaults + .get("enable_memtable") + .map(|v| v == "false") + .unwrap_or(false); + + // Override the active shard with the cached writer's in-memory view. + let mut in_memory: HashMap = HashMap::new(); + if !wal_only + && let Some((shard_id, manifest, memtables)) = + table.dataset.shard_writer().read_snapshot().await? + { + if let Some(manifest) = manifest { + let snapshot = snapshot_from_manifest(shard_id, &manifest, &exclude); + match snapshots.iter_mut().find(|s| s.shard_id == shard_id) { + Some(existing) => *existing = snapshot, + None => snapshots.push(snapshot), + } + } + if let Some(memtables) = memtables { + in_memory.insert(shard_id, memtables); + } + } + + Ok((snapshots, in_memory)) +} + +/// Convert a shard manifest into a read snapshot (current + not-yet-compacted +/// flushed SSTables). SSTable generations at or below the shard's compaction +/// watermark are already in the base table and are skipped. +fn snapshot_from_manifest( + shard_id: Uuid, + manifest: &ShardManifest, + compacted: &HashMap, +) -> ShardSnapshot { + let mut snapshot = ShardSnapshot::new(shard_id) + .with_spec_id(manifest.shard_spec_id) + .with_current_generation(manifest.current_generation); + let watermark = compacted.get(&shard_id).copied(); + for sstable in &manifest.sstables { + if watermark.is_some_and(|w| sstable.generation <= w) { + continue; + } + snapshot = snapshot.with_sstable(sstable.generation, sstable.path.clone()); + } + snapshot +} + +/// Columns selected by the query, if an explicit projection was requested. +fn selected_columns(query: &VectorQueryRequest) -> Option> { + match &query.base.select { + Select::Columns(columns) => Some(columns.clone()), + _ => None, + } +} + +/// Non-negative `Option` limit/offset as the `Option` the scanner +/// expects. +fn as_i64(value: Option) -> Option { + value.map(|v| v as i64) +} + +/// Build a base `LsmScanner` configured with sources, filter, and projection. +/// +/// The filter set here is applied as a prefilter across every arm — plain scan, +/// full-text search, and vector search — since all three terminate on this +/// scanner's `create_plan`. +fn base_scanner( + dataset: &Dataset, + query: &VectorQueryRequest, + pk_columns: Vec, + snapshots: Vec, + in_memory: HashMap, +) -> Result { + let mut scanner = LsmScanner::new(Arc::new(dataset.clone()), snapshots, pk_columns); + for (shard_id, memtables) in in_memory { + scanner = scanner.with_in_memory_memtables(shard_id, memtables); + } + if let Some(columns) = selected_columns(query) { + let refs: Vec<&str> = columns.iter().map(String::as_str).collect(); + scanner = scanner.project(&refs)?; + } + if let Some(filter) = &query.base.filter { + scanner = match filter { + QueryFilter::Sql(sql) => scanner.filter(sql)?, + QueryFilter::Datafusion(expr) => scanner.filter_expr(expr.clone()), + QueryFilter::Substrait(_) => { + return Err(Error::NotSupported { + message: "the MemWAL LSM scanner does not support Substrait filters; set use_lsm(false) to read the base table only".to_string(), + }); + } + }; + } + Ok(scanner) +} + +/// Plain scan: filter / projection / limit over base ∪ SSTables ∪ in-memory. +/// The plain scan applies limit and offset inside the planner. +async fn plain_plan( + dataset: &Dataset, + query: &VectorQueryRequest, + pk_columns: Vec, + snapshots: Vec, + in_memory: HashMap, + limit: Option, + offset: Option, +) -> Result> { + let scanner = base_scanner(dataset, query, pk_columns, snapshots, in_memory)? + .limit(as_i64(limit), as_i64(offset))?; + Ok(scanner.create_plan().await?) +} + +/// Full-text search over base ∪ SSTables ∪ in-memory, merged by local BM25 score. +/// The scanner threads the query filter in as a prefilter and pages via limit/offset. +#[allow(clippy::too_many_arguments)] +async fn fts_plan( + dataset: &Dataset, + fts: lance_index::scalar::FullTextSearchQuery, + query: &VectorQueryRequest, + details: &MemWalIndexDetails, + pk_columns: Vec, + snapshots: Vec, + in_memory: HashMap, + limit: Option, + offset: Option, +) -> Result> { + // Pre-check for a lancedb-flavored error; `LsmScanner` also validates the + // single-column requirement, but without the `use_lsm(false)` guidance. + let columns: Vec = fts.columns().into_iter().collect(); + if columns.len() > 1 { + return Err(Error::NotSupported { + message: "the MemWAL LSM scanner full-text search supports a single column; set use_lsm(false) to read the base table only".to_string(), + }); + } + let column = columns.first().ok_or_else(|| Error::NotSupported { + message: "the MemWAL LSM scanner full-text search requires an explicit FTS column" + .to_string(), + })?; + + // Without a maintained in-memory FTS index for this column, the active memtable + // arm produces an empty plan (`active_source_can_execute_fts` returns false), so + // the search silently omits un-compacted documents. Reject rather than mislead. + if !index_maintained( + dataset, + column, + &details.maintained_indexes, + "InvertedIndexDetails", + ) + .await? + { + return Err(Error::NotSupported { + message: format!( + "the MemWAL LSM scanner full-text search requires the FTS index on '{column}' to be maintained by the write spec (LsmWriteSpec::with_maintained_indexes); otherwise un-compacted documents are omitted. set use_lsm(false) to read the base table only" + ), + }); + } + + let scanner = base_scanner(dataset, query, pk_columns, snapshots, in_memory)? + .with_overfetch_factor(LSM_OVERFETCH_FACTOR) + .full_text_search(fts)? + .limit(as_i64(limit), as_i64(offset))?; + Ok(scanner.create_plan().await?) +} + +/// Whether an index of `type_url_suffix` covering `column` is in the MemWAL spec's +/// maintained set. Only a maintained index has its catch-up tracked (so exclusion is +/// gated correctly) and its in-memory arm kept current; an unmaintained base index +/// falls back to the compaction watermark and can drop rows it has not re-indexed. +/// The type must match specifically — a maintained BTree on the same column is not +/// the FTS/vector index the arm relies on. +async fn index_maintained( + dataset: &Dataset, + column: &str, + maintained: &[String], + type_url_suffix: &str, +) -> Result { + use lance::index::DatasetIndexExt; + let Some(field) = dataset.schema().field(column) else { + return Ok(false); + }; + let indices = dataset.load_indices().await?; + Ok(indices.iter().any(|idx| { + idx.fields.contains(&field.id) + && maintained.iter().any(|m| m == &idx.name) + && idx + .index_details + .as_ref() + .is_some_and(|d| d.type_url.ends_with(type_url_suffix)) + })) +} + +/// The maintained base index the query's arm relies on (vector index for ANN, FTS +/// index for full-text), used to gate SSTable compaction exclusion by index catch-up. +/// `None` for a plain scan or when no maintained index covers the searched column. +async fn arm_maintained_index_name( + dataset: &Dataset, + query: &VectorQueryRequest, + details: &MemWalIndexDetails, +) -> Result> { + use lance::index::DatasetIndexExt; + // Resolve the arm's searched column, the index-detail type it relies on, and a + // label for diagnostics — catch-up is taken from the vector/FTS index + // specifically, not a BTree on the same column. + let (column, type_url_suffix, arm) = if !query.query_vector.is_empty() { + let arrow_schema = ArrowSchema::from(dataset.schema()); + let column = match &query.column { + Some(column) => column.clone(), + None => { + let dim = query.query_vector.first().map(|v| v.len() as i32); + default_vector_column(&arrow_schema, dim)? + } + }; + (column, "VectorIndexDetails", "vector") + } else if let Some(fts) = &query.base.full_text_search { + match fts.columns().into_iter().next() { + Some(column) => (column, "InvertedIndexDetails", "full-text"), + None => return Ok(None), + } + } else { + return Ok(None); + }; + let Some(field) = dataset.schema().field(&column) else { + return Ok(None); + }; + let indices = dataset.load_indices().await?; + let segment_names: Vec = indices + .iter() + .filter(|idx| { + idx.fields.contains(&field.id) + && idx + .index_details + .as_ref() + .is_some_and(|d| d.type_url.ends_with(type_url_suffix)) + }) + .map(|idx| idx.name.clone()) + .collect(); + resolve_single_index(segment_names, &details.maintained_indexes, arm, &column) +} + +/// Resolve the single logical index from the names of its matching physical +/// segments. `load_indices` returns one entry per segment, so one logical index can +/// appear multiple times (same name); dedupe by name before counting. Errors when +/// more than one *distinct* index covers the field — the base planner's choice is +/// ambiguous and their catch-up watermarks can diverge, so gating exclusion on the +/// wrong one could drop SSTables the used index has not caught up to. Otherwise +/// returns the name only when it is maintained (else the caller falls back to the +/// compaction watermark). +fn resolve_single_index( + mut names: Vec, + maintained: &[String], + arm: &str, + column: &str, +) -> Result> { + names.sort(); + names.dedup(); + if names.len() > 1 { + return Err(Error::NotSupported { + message: format!( + "the MemWAL LSM scanner cannot resolve the {arm} index catch-up watermark for '{column}': it has multiple {arm} indexes; set use_lsm(false) to read the base table only" + ), + }); + } + Ok(names + .into_iter() + .next() + .filter(|name| maintained.contains(name))) +} + +/// Drop the primary-key columns Lance appends internally for dedup when the user's +/// explicit projection did not request them, restoring the requested output schema. +/// `Select::All` legitimately includes the pk columns and is left untouched. +fn restore_projection( + plan: Arc, + query: &VectorQueryRequest, + pk_columns: &[String], +) -> Result> { + let Select::Columns(selected) = &query.base.select else { + return Ok(plan); + }; + let schema = plan.schema(); + // Keep a column unless it is a pk column the user did not select (this preserves + // user columns and score columns like `_distance`, dropping only leaked pk). + let keep: Vec<(Arc, String)> = schema + .fields() + .iter() + .enumerate() + .filter(|(_, f)| { + selected.iter().any(|c| c == f.name()) || !pk_columns.iter().any(|pk| pk == f.name()) + }) + .map(|(i, f)| { + ( + Arc::new(Column::new(f.name(), i)) as Arc, + f.name().clone(), + ) + }) + .collect(); + if keep.len() == schema.fields().len() { + return Ok(plan); + } + Ok(Arc::new(ProjectionExec::try_new(keep, plan)?)) +} + +/// Vector (ANN) search over base ∪ SSTables ∪ in-memory, routed through the same +/// [`LsmScanner`] as the other arms so the query filter is applied as a prefilter. +/// +/// Note: the base and SSTable arms use `fast_search` (indexed data only), so a +/// base-table row not covered by a vector index is invisible here — it surfaces +/// only via the memtable or `use_lsm(false)`. +#[allow(clippy::too_many_arguments)] +async fn vector_plan( + dataset: &Dataset, + query: &VectorQueryRequest, + details: &MemWalIndexDetails, + pk_columns: Vec, + snapshots: Vec, + in_memory: HashMap, + limit: Option, + offset: Option, +) -> Result> { + let query_vector = query + .query_vector + .first() + .cloned() + .ok_or_else(|| Error::InvalidInput { + message: "vector search requires a query vector".to_string(), + })?; + + let arrow_schema = ArrowSchema::from(dataset.schema()); + let column = match &query.column { + Some(column) => column.clone(), + None => default_vector_column(&arrow_schema, Some(query_vector.len() as i32))?, + }; + + // The base arm relies on the column's vector index (`fast_search`). Unless it is + // maintained, its catch-up is untracked and exclusion falls back to the + // compaction watermark — dropping compacted SSTables the (lagging) base index has + // not re-indexed. Reject rather than silently omit rows, mirroring the FTS arm. + if !index_maintained( + dataset, + &column, + &details.maintained_indexes, + "VectorIndexDetails", + ) + .await? + { + return Err(Error::NotSupported { + message: format!( + "the MemWAL LSM scanner requires the vector index on '{column}' to be maintained by the write spec (LsmWriteSpec::with_maintained_indexes); otherwise compacted rows not yet re-indexed are omitted. set use_lsm(false) to read the base table only" + ), + }); + } + + // The LSM vector planner is Float32-only; reject binary (uint8) vectors with a + // clear error rather than failing deep in the planner. + if is_binary_vector_column(&arrow_schema, &column) { + return Err(Error::NotSupported { + message: "the MemWAL LSM scanner does not support binary (uint8) vector search; set use_lsm(false) to read the base table only".to_string(), + }); + } + + let distance_type = resolve_distance_type(dataset, query, &column).await?; + + // `nearest` takes a flat query vector and builds the fixed-size list itself. + // Guard `k` to at least 1 so a degenerate `limit(0)` is trimmed by `limit` + // below rather than rejected by `nearest`. + let k = limit.unwrap_or(DEFAULT_TOP_K).max(1); + let mut scanner = base_scanner(dataset, query, pk_columns, snapshots, in_memory)? + .with_overfetch_factor(LSM_OVERFETCH_FACTOR) + .nearest(&column, query_vector.as_ref(), k)? + .nprobes(query.minimum_nprobes) + .distance_metric(distance_type.into()); + if let Some(refine_factor) = query.refine_factor { + scanner = scanner.refine(refine_factor); + } + scanner = scanner.limit(as_i64(limit), as_i64(offset))?; + Ok(scanner.create_plan().await?) +} + +/// Whether `column` stores binary (uint8) vectors, which the LSM vector planner +/// does not support. +fn is_binary_vector_column(schema: &ArrowSchema, column: &str) -> bool { + matches!( + schema.field_with_name(column).map(|f| f.data_type()), + Ok(DataType::FixedSizeList(field, _)) if matches!(field.data_type(), DataType::UInt8) + ) +} + +/// Resolve the distance metric for the vector arm: the explicit query metric if +/// set, else the metric of the column's vector index, else L2. +async fn resolve_distance_type( + dataset: &Dataset, + query: &VectorQueryRequest, + column: &str, +) -> Result { + if let Some(dt) = query.distance_type { + return Ok(dt); + } + // Inherit the column's vector-index metric so cross-source distances match + // the metric the maintained memtable index was built with. + use lance::index::{DatasetIndexExt, DatasetIndexInternalExt}; + use lance_index::metrics::NoOpMetricsCollector; + let field = dataset.schema().field(column); + if let Some(field) = field { + let indices = dataset.load_indices().await?; + for index in indices.iter() { + if index.fields.contains(&field.id) + && let Ok(vector_index) = dataset + .open_vector_index(column, &index.uuid, &NoOpMetricsCollector) + .await + { + return Ok(vector_index.metric_type().into()); + } + } + } + Ok(DistanceType::L2) +} + +#[cfg(test)] +mod tests { + use super::*; + use lance_index::mem_wal::{CompactedSsTable, IndexCatchupProgress}; + + #[test] + fn exclusion_watermark_gates_on_lagging_index_catchup() { + let shard = Uuid::from_u128(1); + let details = MemWalIndexDetails { + // Compaction has drained generations through 5 into the base table... + compacted_sstables: vec![CompactedSsTable::new(shard, 5)], + // ...but the FTS index has only caught up through generation 2. + index_catchup: vec![IndexCatchupProgress::new( + "fts_idx".to_string(), + vec![CompactedSsTable::new(shard, 2)], + )], + maintained_indexes: vec!["fts_idx".to_string()], + ..Default::default() + }; + + // Plain scan: drop every compacted generation (through 5). + assert_eq!(exclusion_watermarks(&details, None).get(&shard), Some(&5)); + + // FTS arm with a lagging index: exclusion is capped at the index catch-up + // (2), so SSTable generations 3..=5 are retained until the index covers + // them — otherwise those documents would silently vanish from FTS results. + assert_eq!( + exclusion_watermarks(&details, Some("fts_idx")).get(&shard), + Some(&2) + ); + + // A caught-up index — or one untracked in index_catchup — falls back to the + // compaction watermark. + assert_eq!( + exclusion_watermarks(&details, Some("caught_up_idx")).get(&shard), + Some(&5) + ); + } + + #[test] + fn resolve_single_index_dedupes_segments() { + let maintained = vec!["fts_idx".to_string()]; + // Two physical segments of ONE logical index must not count as "multiple". + assert_eq!( + resolve_single_index( + vec!["fts_idx".to_string(), "fts_idx".to_string()], + &maintained, + "full-text", + "text" + ) + .unwrap(), + Some("fts_idx".to_string()) + ); + // Two distinct indexes on the field are ambiguous → error. + assert!( + resolve_single_index( + vec!["fts_a".to_string(), "fts_b".to_string()], + &maintained, + "full-text", + "text" + ) + .is_err() + ); + // A single unmaintained index resolves to None (compaction-watermark fallback). + assert_eq!( + resolve_single_index(vec!["other".to_string()], &maintained, "full-text", "text") + .unwrap(), + None + ); + } +} From ba4558a64f23f93ea42b4f78a9889bf0677977a3 Mon Sep 17 00:00:00 2001 From: LanceDB Robot Date: Mon, 27 Jul 2026 00:27:40 -0700 Subject: [PATCH 03/29] chore: update lance dependency to v10.0.0-beta.5 (#3718) Updates the Rust workspace Lance dependencies and Java lance-core dependency to v10.0.0-beta.5. No compatibility fixes were required; full-workspace Clippy passes with warnings denied. Lance tag: https://github.com/lance-format/lance/releases/tag/v10.0.0-beta.5 --- Cargo.lock | 97 +++++++++++++++++++++++++++++----------------------- Cargo.toml | 28 +++++++-------- java/pom.xml | 2 +- 3 files changed, 70 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8bde1b00..e80165a70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3421,8 +3421,8 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsst" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow-array", "rand 0.9.5", @@ -4777,8 +4777,8 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" [[package]] name = "lance" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arc-swap", "arrow", @@ -4852,8 +4852,8 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow-array", "arrow-buffer", @@ -4875,7 +4875,7 @@ dependencies = [ [[package]] name = "lance-arrow-scalar" version = "58.0.0" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow-array", "arrow-buffer", @@ -4889,7 +4889,7 @@ dependencies = [ [[package]] name = "lance-arrow-stats" version = "58.0.0" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow-array", "arrow-schema", @@ -4898,8 +4898,8 @@ dependencies = [ [[package]] name = "lance-bitpacking" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrayref", "crunchy", @@ -4909,8 +4909,8 @@ dependencies = [ [[package]] name = "lance-core" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow-array", "arrow-buffer", @@ -4933,6 +4933,7 @@ dependencies = [ "object_store", "pin-project", "prost", + "quick_cache", "rand 0.9.5", "roaring", "serde_json", @@ -4948,8 +4949,8 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow", "arrow-array", @@ -4979,8 +4980,8 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow", "arrow-array", @@ -4997,8 +4998,8 @@ dependencies = [ [[package]] name = "lance-derive" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "proc-macro2", "quote", @@ -5007,8 +5008,8 @@ dependencies = [ [[package]] name = "lance-encoding" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow-arith", "arrow-array", @@ -5043,8 +5044,8 @@ dependencies = [ [[package]] name = "lance-file" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow-arith", "arrow-array", @@ -5074,8 +5075,8 @@ dependencies = [ [[package]] name = "lance-index" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arc-swap", "arrow", @@ -5142,8 +5143,8 @@ dependencies = [ [[package]] name = "lance-index-core" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow-array", "arrow-schema", @@ -5165,8 +5166,8 @@ dependencies = [ [[package]] name = "lance-io" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow", "arrow-arith", @@ -5209,8 +5210,8 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow-array", "arrow-buffer", @@ -5226,8 +5227,8 @@ dependencies = [ [[package]] name = "lance-namespace" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow", "async-trait", @@ -5239,8 +5240,8 @@ dependencies = [ [[package]] name = "lance-namespace-impls" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow", "arrow-ipc", @@ -5294,8 +5295,8 @@ dependencies = [ [[package]] name = "lance-select" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow-array", "arrow-buffer", @@ -5310,8 +5311,8 @@ dependencies = [ [[package]] name = "lance-table" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow", "arrow-array", @@ -5350,8 +5351,8 @@ dependencies = [ [[package]] name = "lance-testing" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "arrow-array", "arrow-schema", @@ -5364,8 +5365,8 @@ dependencies = [ [[package]] name = "lance-tokenizer" -version = "10.0.0-beta.3" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.3#ed078e7c19c9560906e1eae54b0baf01dc509986" +version = "10.0.0-beta.5" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" dependencies = [ "icu_segmenter", "jieba-rs", @@ -7802,6 +7803,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick_cache" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9c6658afe513a3b484e3abfdaa0d03ef3c0bbf017542c178dd55f94eb3051f9" +dependencies = [ + "ahash", + "equivalent", + "hashbrown 0.16.1", + "parking_lot", +] + [[package]] name = "quinn" version = "0.11.9" diff --git a/Cargo.toml b/Cargo.toml index c65d6590e..fee01b732 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,20 +13,20 @@ categories = ["database-implementations"] rust-version = "1.91.0" [workspace.dependencies] -lance = { "version" = "=10.0.0-beta.3", default-features = false, "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-core = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-datagen = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-file = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-io = { "version" = "=10.0.0-beta.3", default-features = false, "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-index = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-linalg = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-namespace = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-namespace-impls = { "version" = "=10.0.0-beta.3", default-features = false, "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-table = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-testing = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-datafusion = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-encoding = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } -lance-arrow = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" } +lance = { "version" = "=10.0.0-beta.5", default-features = false, "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-core = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-datagen = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-file = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-io = { "version" = "=10.0.0-beta.5", default-features = false, "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-index = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-linalg = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-namespace = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-namespace-impls = { "version" = "=10.0.0-beta.5", default-features = false, "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-table = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-testing = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-datafusion = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-encoding = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance-arrow = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } ahash = "0.8" # Note that this one does not include pyarrow arrow = { version = "58.0.0", optional = false } diff --git a/java/pom.xml b/java/pom.xml index a6ebf1165..ab8ec2cc8 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -28,7 +28,7 @@ UTF-8 15.0.0 - 10.0.0-beta.3 + 10.0.0-beta.5 false 2.30.0 1.7 From 119b9baf905c33ec52e7509c3dab081f7be4655c Mon Sep 17 00:00:00 2001 From: Vivek Date: Mon, 27 Jul 2026 12:25:11 -0700 Subject: [PATCH 04/29] fix: preserve row count in MetadataEraserExec for zero-column batches (#3717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SELECT COUNT(*) FROM t WHERE — and any query that plans an empty-projection scan — panics the executing query task: InvalidArgumentError("must either specify a row count or at least one column") Root cause MetadataEraserExec wraps every LanceDB table scan to strip schema-level metadata, rebuilding each batch in execute(): RecordBatch::try_new(schema.clone(), batch.columns().to_vec()).unwrap() RecordBatch::try_new infers the row count from the columns. COUNT(*) with a filter is planned with an empty projection, so the scan emits zero-column batches — there are no columns to infer a length from, try_new returns Err, and the .unwrap() panics. (This is specific to the empty-projection case: COUNT(*) with no filter is answered from statistics and never scans, and COUNT() projects a column — both already work.) --- rust/lancedb/src/table/datafusion.rs | 63 +++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/rust/lancedb/src/table/datafusion.rs b/rust/lancedb/src/table/datafusion.rs index 4272df16d..d60bce7dc 100644 --- a/rust/lancedb/src/table/datafusion.rs +++ b/rust/lancedb/src/table/datafusion.rs @@ -12,7 +12,7 @@ pub mod udtf; use std::{collections::HashMap, sync::Arc}; -use arrow_array::RecordBatch; +use arrow_array::{RecordBatch, RecordBatchOptions}; use arrow_schema::Schema as ArrowSchema; use async_trait::async_trait; use datafusion_catalog::{Session, TableProvider}; @@ -126,7 +126,12 @@ impl ExecutionPlan for MetadataEraserExec { let stream = self.input.execute(partition, context)?; let schema = self.schema.clone(); let stream = stream.map_ok(move |batch| { - RecordBatch::try_new(schema.clone(), batch.columns().to_vec()).unwrap() + RecordBatch::try_new_with_options( + schema.clone(), + batch.columns().to_vec(), + &RecordBatchOptions::new().with_row_count(Some(batch.num_rows())), + ) + .unwrap() }); Ok( Box::pin(RecordBatchStreamAdapter::new(self.schema.clone(), stream)) @@ -544,6 +549,60 @@ pub mod tests { } } + /// A scan with an EMPTY projection (the shape a filtered `COUNT(*)` feeds in) yields + /// zero-column batches. `MetadataEraserExec::execute` rebuilt each batch with + /// `RecordBatch::try_new(schema, cols).unwrap()`; for a column-less batch that errors with + /// "must either specify a row count or at least one column" and the `.unwrap()` panics. + #[tokio::test] + async fn test_metadata_eraser_empty_projection_preserves_row_count() { + let fixture = TestFixture::new().await; + + // Empty projection => zero output columns over N rows. Table "foo" has 10 rows. + let plan = + LogicalPlanBuilder::scan("foo", provider_as_source(fixture.adapter), Some(vec![])) + .unwrap() + .build() + .unwrap(); + + let mut stream = TestFixture::plan_to_stream(plan).await; + + let mut rows = 0usize; + while let Some(batch) = stream.try_next().await.unwrap() { + assert_eq!( + batch.num_columns(), + 0, + "empty projection must yield zero columns" + ); + rows += batch.num_rows(); + } + assert_eq!( + rows, 10, + "row count must survive MetadataEraserExec on a zero-column batch" + ); + + // End-to-end SQL regression for the previous panic. + let fixture = TestFixture::new().await; + + let ctx = SessionContext::new(); + ctx.register_table("foo", fixture.adapter.clone()).unwrap(); + + let batches = ctx + .sql("SELECT COUNT(*) FROM foo WHERE i < 5") + .await + .unwrap() + .collect() + .await + .unwrap(); + + let count = batches[0] + .column(0) + .as_any() + .downcast_ref::() + .unwrap() + .value(0); + assert_eq!(count, 5, "COUNT(*) WHERE i < 5 over 0..10 must be 5"); + } + #[tokio::test] async fn test_filter_pushdown() { let fixture = TestFixture::new().await; From ff6ff099988393400017f80a0e95b1d5eab4389d Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Tue, 28 Jul 2026 06:08:47 +0800 Subject: [PATCH 05/29] feat: support batched blob range reads (#3703) ## Summary Lance can now plan multiple byte ranges for the same blob in one `read_blob_ranges` operation, but LanceDB users currently cannot expose a complete set of logical ranges to that planner. This complements `BlobFile`: file-like consumers such as PyAV can continue to discover ranges dynamically, while callers that already know the ranges for a batch can submit them together. ## Motivating example A training table may store a large video blob together with a small application-level clip index: ```text video: blob clips: [{offset, length}, ...] ``` The caller can select the videos and clips for a batch, obtain their row IDs from the query, and read all of the selected windows together: ```python rows = ( table.search() .select(["clips"]) .with_row_id(True) .limit(64) .to_arrow() .to_pylist() ) requests = [] for row in rows: clip = sample_clip(row["clips"]) requests.append( (row["_rowid"], clip["offset"], clip["length"]) ) chunks = table.fetch_blob_ranges("video", requests) ``` Here, `_rowid` comes from the LanceDB query, while `offset` and `length` come from the application's clip index and are relative to that row's video blob. The caller describes only the logical reads; Lance still handles validation, source grouping, coalescing, scheduling, and byte backpressure. Lance v10.0.0-beta.5 returns one logical result per blob selector or range request and explicitly distinguishes null blobs from valid empty values. LanceDB consumes that aligned result contract directly and only adds a cardinality check for unresolved row IDs. This PR exposes batched blob-range reads on local Rust and Python tables. Results preserve request identity, duplicates, null slots, and valid empty ranges while allowing Lance to execute the physical reads out of order. Scheduler buffer sizing remains an internal Lance concern, so the LanceDB API does not expose `io_buffer_size`. Cloud tables continue to report this operation as unsupported until there is a corresponding remote API. --- python/python/lancedb/_lancedb.pyi | 5 + python/python/lancedb/remote/table.py | 5 + python/python/lancedb/table.py | 35 ++++ python/python/tests/test_blob.py | 65 ++++++- python/src/table.rs | 23 ++- rust/lancedb/src/blob.rs | 226 +++++++++---------------- rust/lancedb/src/lib.rs | 2 +- rust/lancedb/src/table.rs | 66 +++++++- rust/lancedb/tests/blob_integration.rs | 134 ++++++++++++++- 9 files changed, 408 insertions(+), 153 deletions(-) diff --git a/python/python/lancedb/_lancedb.pyi b/python/python/lancedb/_lancedb.pyi index 9c000a831..448947a29 100644 --- a/python/python/lancedb/_lancedb.pyi +++ b/python/python/lancedb/_lancedb.pyi @@ -297,6 +297,11 @@ class Table: async def fetch_blobs( self, column: str, row_ids: list[int] ) -> pa.LargeBinaryArray: ... + async def fetch_blob_ranges( + self, + column: str, + requests: List[Tuple[int, int, int]], + ) -> pa.LargeBinaryArray: ... async def fetch_blob_files( self, column: str, row_ids: list[int] ) -> list[Optional[BlobFile]]: ... diff --git a/python/python/lancedb/remote/table.py b/python/python/lancedb/remote/table.py index 80c250bf5..682b8533c 100644 --- a/python/python/lancedb/remote/table.py +++ b/python/python/lancedb/remote/table.py @@ -1042,6 +1042,11 @@ class RemoteTable(Table): def fetch_blobs(self, column: str, row_ids) -> pa.LargeBinaryArray: raise NotImplementedError("fetch_blobs() is not supported on LanceDB Cloud") + def fetch_blob_ranges(self, column: str, requests) -> pa.LargeBinaryArray: + raise NotImplementedError( + "fetch_blob_ranges() is not supported on LanceDB Cloud" + ) + def fetch_blob_files(self, column: str, row_ids): raise NotImplementedError( "fetch_blob_files() is not supported on LanceDB Cloud" diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index f61a1f1cf..6c3854734 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -20,6 +20,7 @@ from typing import ( List, Literal, Optional, + Sequence, Tuple, Union, overload, @@ -1538,10 +1539,30 @@ class Table(ABC): ) -> pa.LargeBinaryArray: """Materialize full blob bytes for ``column`` at the given rows. + The result has the same length and order as ``row_ids``. Null blobs + produce null slots; valid empty blobs produce ``b""``. + Convenience for small payloads. For large values use :meth:`fetch_blob_files`. """ + @abstractmethod + def fetch_blob_ranges( + self, + column: str, + requests: Sequence[Tuple[int, int, int]], + ) -> pa.LargeBinaryArray: + """Materialize row-specific byte ranges from a blob v2 column. + + Each request is a ``(row_id, offset, length)`` tuple. Requests may be + repeated or reordered, including multiple ranges for the same blob. + The result has the same length and order as ``requests``; null blobs + produce null slots and empty ranges on non-null blobs produce ``b""``. + + Row IDs can be obtained from a query with ``with_row_id(True)``. This + API is currently supported only by local tables. + """ + @abstractmethod def fetch_blob_files( self, column: str, row_ids: Union[list[int], pa.Table] @@ -2265,6 +2286,13 @@ class LanceTable(Table): ) -> pa.LargeBinaryArray: return LOOP.run(self._table.fetch_blobs(column, row_ids)) + def fetch_blob_ranges( + self, + column: str, + requests: Sequence[Tuple[int, int, int]], + ) -> pa.LargeBinaryArray: + return LOOP.run(self._table.fetch_blob_ranges(column, list(requests))) + def fetch_blob_files( self, column: str, row_ids: Union[list[int], pa.Table] ) -> "list[Optional[BlobFile]]": @@ -5829,6 +5857,13 @@ class AsyncTable: column, _normalize_blob_row_ids(row_ids, column) ) + async def fetch_blob_ranges( + self, + column: str, + requests: Sequence[Tuple[int, int, int]], + ) -> pa.LargeBinaryArray: + return await self._inner.fetch_blob_ranges(column, list(requests)) + async def fetch_blob_files( self, column: str, row_ids: Union[list[int], pa.Table] ) -> "list[Optional[BlobFile]]": diff --git a/python/python/tests/test_blob.py b/python/python/tests/test_blob.py index 05af14298..04a99ea16 100644 --- a/python/python/tests/test_blob.py +++ b/python/python/tests/test_blob.py @@ -184,18 +184,75 @@ def test_fetch_blobs_accepts_query_result(): assert {blobs[i].as_py() for i in range(len(blobs))} == {b"gamma"} -def test_fetch_blobs_null_alignment(): +def test_fetch_blobs_preserves_null_and_empty_values(): table = _blob_table( "nulls", - [{"id": 1, "image": b"present"}, {"id": 2, "image": None}], + [ + {"id": 1, "image": b"present"}, + {"id": 2, "image": None}, + {"id": 3, "image": b""}, + ], ) by_id = _row_ids_by_id(table) - request = [by_id[1], by_id[2], by_id[1]] + request = [by_id[1], by_id[2], by_id[3], by_id[1]] blobs = table.fetch_blobs("image", request) assert len(blobs) == len(request) assert blobs[0].as_py() == b"present" assert blobs[1].as_py() is None - assert blobs[2].as_py() == b"present" + assert blobs[2].as_py() == b"" + assert blobs[3].as_py() == b"present" + + +def test_fetch_blob_ranges_aligns_repeated_ranges_and_nulls(): + table = _blob_table( + "range_alignment", + [{"id": 1, "image": b"abcdefghij"}, {"id": 2, "image": None}], + ) + by_id = _row_ids_by_id(table) + requests = [ + (by_id[1], 2, 3), + (by_id[2], 0, 0), + (by_id[1], 0, 2), + (by_id[1], 2, 3), + (by_id[1], 10, 0), + ] + + ranges = table.fetch_blob_ranges("image", requests) + + assert ranges.to_pylist() == [b"cde", None, b"ab", b"cde", b""] + + +def test_fetch_blob_ranges_validates_requests(): + table = _blob_table("range_validation", [{"id": 1, "image": b"abc"}]) + row_id = _row_ids_by_id(table)[1] + + with pytest.raises(RuntimeError, match="exceeds blob size"): + table.fetch_blob_ranges("image", [(row_id, 2, 2)]) + + with pytest.raises(RuntimeError, match="offset \\+ length overflowed"): + table.fetch_blob_ranges("image", [(row_id, 2**64 - 1, 1)]) + + with pytest.raises(ValueError, match="row ids"): + table.fetch_blob_ranges("image", [(2**64 - 1, 0, 1)]) + + +def test_fetch_blob_ranges_empty_requests_returns_empty_array(): + table = _blob_table("range_empty", [{"id": 1, "image": b"x"}]) + assert table.fetch_blob_ranges("image", []).to_pylist() == [] + + +@pytest.mark.asyncio +async def test_async_fetch_blob_ranges(): + db = await lancedb.connect_async("memory:///") + schema = pa.schema([pa.field("id", pa.int64()), lancedb.blob("image")]) + table = await db.create_table("range_async", schema=schema) + await table.add([{"id": 1, "image": b"abcdefghij"}]) + hits = await table.query().with_row_id().to_arrow() + row_id = hits["_rowid"][0].as_py() + + ranges = await table.fetch_blob_ranges("image", [(row_id, 1, 3), (row_id, 6, 2)]) + + assert ranges.to_pylist() == [b"bcd", b"gh"] def test_fetch_blobs_nested_path(): diff --git a/python/src/table.rs b/python/src/table.rs index b62aa7377..bb44fc023 100644 --- a/python/src/table.rs +++ b/python/src/table.rs @@ -17,7 +17,7 @@ use arrow::{ ffi_stream::ArrowArrayStreamReader, pyarrow::{FromPyArrow, PyArrowType, ToPyArrow}, }; -use lancedb::blob::BlobFile; +use lancedb::blob::{BlobFile, BlobRangeRequest}; use lancedb::index::scalar::FtsIndexBuilder; use lancedb::table::{ AddDataMode, ColumnAlteration, Duration, FieldMetadataUpdate, FtsToken as LanceDbFtsToken, @@ -1101,6 +1101,27 @@ impl Table { }) } + /// Read row-specific blob-local byte ranges in one planned operation. + #[pyo3(signature = (column, requests))] + pub fn fetch_blob_ranges( + self_: PyRef<'_, Self>, + column: String, + requests: Vec<(u64, u64, u64)>, + ) -> PyResult> { + let inner = self_.inner_ref()?.clone(); + future_into_py(self_.py(), async move { + let requests = requests + .into_iter() + .map(|(row_id, offset, length)| BlobRangeRequest::new(row_id, offset, length)) + .collect::>(); + let blobs: LargeBinaryArray = inner + .fetch_blob_ranges(column, requests) + .await + .infer_error()?; + Python::attach(|py| blobs.to_data().to_pyarrow(py).map(|obj| obj.unbind())) + }) + } + /// Open lazy blob handles for `row_ids` from blob v2 column `column`. #[pyo3(signature = (column, row_ids))] pub fn fetch_blob_files( diff --git a/rust/lancedb/src/blob.rs b/rust/lancedb/src/blob.rs index de666aa10..a21fe3c1b 100644 --- a/rust/lancedb/src/blob.rs +++ b/rust/lancedb/src/blob.rs @@ -11,18 +11,42 @@ use std::sync::Arc; +use arrow_array::LargeBinaryArray; use arrow_array::builder::LargeBinaryBuilder; -use arrow_array::{Array, LargeBinaryArray, RecordBatch, StructArray, UInt8Array, UInt64Array}; use arrow_schema::{DataType, Field, Schema}; -use lance::dataset::{Dataset, WriteParams}; +use lance::dataset::{BlobRangeRequest as LanceBlobRangeRequest, Dataset, WriteParams}; use lance_arrow::FieldExt; -use lance_core::datatypes::parse_field_path; use lance_encoding::version::LanceFileVersion; use crate::error::{Error, Result}; pub use lance::dataset::BlobFile; +/// One row-specific blob range read request. +/// +/// `row_id` is obtained from a query with row ids enabled. +/// `offset` and `length` are relative to the beginning of the logical blob. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BlobRangeRequest { + /// Row id of the blob value to read. + pub row_id: u64, + /// Byte offset from the beginning of the blob value. + pub offset: u64, + /// Number of bytes to read. + pub length: u64, +} + +impl BlobRangeRequest { + /// Create a row-specific blob range request. + pub const fn new(row_id: u64, offset: u64, length: u64) -> Self { + Self { + row_id, + offset, + length, + } + } +} + /// Creates an Arrow field for a Lance blob v2 column. /// /// `Struct` with the `lance.blob.v2` marker. Same layout Lance @@ -145,91 +169,57 @@ pub(crate) fn ensure_blob_v2_column( } } -/// Returns the leaf descriptor `StructArray` for `column` in a descriptor batch. -fn leaf_descriptor_struct<'a>(batch: &'a RecordBatch, column: &str) -> Result<&'a StructArray> { - let path = parse_field_path(column).map_err(|e| Error::InvalidInput { - message: format!("invalid blob column path '{column}': {e}"), - })?; - let not_struct = || Error::Runtime { - message: format!("blob column '{column}' did not read back as a descriptor struct"), - }; - let mut current = batch - .column_by_name(&path[0]) - .and_then(|c| c.as_any().downcast_ref::()) - .ok_or_else(not_struct)?; - for segment in &path[1..] { - current = current - .column_by_name(segment) - .and_then(|c| c.as_any().downcast_ref::()) - .ok_or_else(not_struct)?; +fn ensure_all_row_ids_resolved(column: &str, requested: usize, resolved: usize) -> Result<()> { + if requested == resolved { + return Ok(()); + } + if resolved < requested { + Err(Error::InvalidInput { + message: format!( + "blob read for column '{column}' requested {requested} row ids but only {resolved} \ + exist in the table; pass row ids collected from this table" + ), + }) + } else { + Err(Error::Runtime { + message: format!( + "blob read for column '{column}' returned {resolved} results for {requested} row ids" + ), + }) } - Ok(current) } -/// Null rows in `row_ids`, from a descriptor take. -/// -/// Lance `read_blobs` / `take_blobs` skip null rows (`kind == 0 && position == 0 && size == 0`). -/// TODO(lance): aligned read API would drop this pass. -async fn blob_null_mask( +/// Materialize blob-local ranges (same length and order as `requests`, nulls preserved). +pub(crate) async fn take_blob_ranges_aligned( dataset: &Arc, column: &str, - row_ids: &[u64], -) -> Result> { - let projection = dataset.schema().project(&[column])?; - let descriptors = dataset.take_builder(row_ids, projection)?.execute().await?; - if descriptors.num_rows() != row_ids.len() { - return Err(Error::InvalidInput { - message: format!( - "blob take for column '{column}' requested {} row ids but only {} exist in the \ - table; pass row ids collected from this table", - row_ids.len(), - descriptors.num_rows() - ), - }); + requests: &[BlobRangeRequest], +) -> Result { + ensure_blob_v2_column(dataset.schema(), column)?; + if requests.is_empty() { + return Ok(LargeBinaryBuilder::new().finish()); } - let descriptor_struct = leaf_descriptor_struct(&descriptors, column)?; - let child = |name: &str| { - descriptor_struct - .column_by_name(name) - .ok_or_else(|| Error::Runtime { - message: format!("blob descriptor for '{column}' is missing the '{name}' field"), - }) - }; - let kinds = child("kind")? - .as_any() - .downcast_ref::() - .ok_or_else(|| Error::Runtime { - message: format!("blob descriptor 'kind' for '{column}' is not a UInt8 array"), - })?; - let positions = child("position")? - .as_any() - .downcast_ref::() - .ok_or_else(|| Error::Runtime { - message: format!("blob descriptor 'position' for '{column}' is not a UInt64 array"), - })?; - let sizes = child("size")? - .as_any() - .downcast_ref::() - .ok_or_else(|| Error::Runtime { - message: format!("blob descriptor 'size' for '{column}' is not a UInt64 array"), - })?; - // Match Lance `collect_blob_entries_v2` skip condition (`BlobKind::Inline` == 0). - Ok((0..descriptor_struct.len()) - .map(|i| { - descriptor_struct.is_null(i) - || kinds.is_null(i) - || (kinds.value(i) == 0 && positions.value(i) == 0 && sizes.value(i) == 0) - }) - .collect()) -} - -fn non_null_row_ids(row_ids: &[u64], null_mask: &[bool]) -> Vec { - row_ids + let lance_requests = requests .iter() - .zip(null_mask) - .filter_map(|(row_id, is_null)| (!is_null).then_some(*row_id)) - .collect() + .map(|request| LanceBlobRangeRequest::new(request.row_id, request.offset, request.length)) + .collect::>(); + let payloads = dataset + .read_blob_ranges(column)? + .with_row_ids(lance_requests) + .preserve_order(true) + .execute() + .await?; + ensure_all_row_ids_resolved(column, requests.len(), payloads.len())?; + + let mut builder = LargeBinaryBuilder::new(); + for payload in payloads { + match payload.data { + Some(data) => builder.append_value(data), + None => builder.append_null(), + } + } + Ok(builder.finish()) } /// Materialize blob bytes for `row_ids` (same length and order, nulls preserved). @@ -243,42 +233,19 @@ pub(crate) async fn take_blobs_aligned( return Ok(LargeBinaryBuilder::new().finish()); } - let null_mask = blob_null_mask(dataset, column, row_ids).await?; - let non_null_row_ids = non_null_row_ids(row_ids, &null_mask); - let non_null_count = non_null_row_ids.len(); - let payloads = if non_null_count == 0 { - Vec::new() - } else { - dataset - .read_blobs(column)? - .with_row_ids(non_null_row_ids) - .preserve_order(true) - .execute() - .await? - }; - - if payloads.len() != non_null_count { - return Err(Error::Runtime { - message: format!( - "blob read for column '{column}' returned {} payloads for {} non-null rows", - payloads.len(), - non_null_count - ), - }); - } + let payloads = dataset + .read_blobs(column)? + .with_row_ids(row_ids.to_vec()) + .preserve_order(true) + .execute() + .await?; + ensure_all_row_ids_resolved(column, row_ids.len(), payloads.len())?; let mut builder = LargeBinaryBuilder::new(); - let mut payload_idx = 0; - for is_null in &null_mask { - if *is_null { - builder.append_null(); - } else { - if let Some(data) = &payloads[payload_idx].data { - builder.append_value(data); - } else { - builder.append_null(); - } - payload_idx += 1; + for payload in payloads { + match payload.data { + Some(data) => builder.append_value(data), + None => builder.append_null(), } } Ok(builder.finish()) @@ -295,34 +262,9 @@ pub(crate) async fn take_blob_files_aligned( return Ok(Vec::new()); } - let null_mask = blob_null_mask(dataset, column, row_ids).await?; - let non_null_row_ids = non_null_row_ids(row_ids, &null_mask); - let handles = if non_null_row_ids.is_empty() { - Vec::new() - } else { - dataset.take_blobs(&non_null_row_ids, column).await? - }; - if handles.len() != non_null_row_ids.len() { - return Err(Error::Runtime { - message: format!( - "blob take for column '{column}' returned {} handles for {} non-null rows", - handles.len(), - non_null_row_ids.len() - ), - }); - } - - let mut handles = handles.into_iter(); - Ok(null_mask - .iter() - .map(|is_null| { - if *is_null { - None - } else { - handles.next().flatten() - } - }) - .collect()) + let handles = dataset.take_blobs(row_ids, column).await?; + ensure_all_row_ids_resolved(column, row_ids.len(), handles.len())?; + Ok(handles) } #[cfg(test)] diff --git a/rust/lancedb/src/lib.rs b/rust/lancedb/src/lib.rs index 35a564d41..3cdf33615 100644 --- a/rust/lancedb/src/lib.rs +++ b/rust/lancedb/src/lib.rs @@ -201,7 +201,7 @@ use std::{fmt::Display, str::FromStr}; use serde::{Deserialize, Serialize}; -pub use blob::{blob, is_blob}; +pub use blob::{BlobRangeRequest, blob, is_blob}; pub use connection::{ConnectNamespaceBuilder, Connection}; pub use error::{Error, Result}; use lance_index::vector::ApproxMode as LanceApproxMode; diff --git a/rust/lancedb/src/table.rs b/rust/lancedb/src/table.rs index 1f7b2f75b..0e9354ffc 100644 --- a/rust/lancedb/src/table.rs +++ b/rust/lancedb/src/table.rs @@ -46,6 +46,7 @@ use std::sync::Arc; use crate::connection::NamespaceClientPushdownOperation; use crate::DistanceType; +use crate::blob::BlobRangeRequest; use crate::data::scannable::{PeekedScannable, Scannable, estimate_write_partitions}; use crate::database::Database; use crate::database::read_freshness::TableFreshness; @@ -646,6 +647,16 @@ pub trait BaseTable: std::fmt::Display + std::fmt::Debug + Send + Sync { message: "fetch_blobs is not supported on this table type".into(), }) } + /// Materialize blob-local ranges. See [`Table::fetch_blob_ranges`]. + async fn fetch_blob_ranges( + &self, + _column: &str, + _requests: &[BlobRangeRequest], + ) -> Result { + Err(Error::NotSupported { + message: "fetch_blob_ranges is not supported on this table type".into(), + }) + } /// Open lazy blob handles for the given row ids. See [`Table::fetch_blob_files`]. async fn fetch_blob_files( &self, @@ -1019,8 +1030,9 @@ impl Table { /// Materialize blob bytes for the given row ids. /// - /// Output matches `row_ids` in length and order. Null and zero-length rows - /// are null. Prefer [`Self::fetch_blob_files`] for large selections. + /// Output matches `row_ids` in length and order. Null blobs are null; + /// valid empty blobs contain empty byte strings. Prefer + /// [`Self::fetch_blob_files`] for large selections. /// /// ``` /// use arrow_array::UInt64Array; @@ -1055,6 +1067,47 @@ impl Table { self.inner.fetch_blobs(column.as_ref(), row_ids).await } + /// Materialize row-specific ranges from a blob v2 column. + /// + /// Each request contains a row id and a blob-local offset and length. + /// Requests may be duplicated or reordered, including multiple + /// ranges for the same blob. The output has the same length and order as + /// the requests. Null blobs produce null output slots; empty ranges on + /// non-null blobs produce empty byte strings. + /// + /// ``` + /// use lancedb::blob::BlobRangeRequest; + /// + /// # use lancedb::Table; + /// # async fn read_ranges(table: &Table, row_id: u64) -> Result<(), Box> { + /// let ranges = table + /// .fetch_blob_ranges( + /// "image", + /// [ + /// BlobRangeRequest::new(row_id, 0, 1024), + /// BlobRangeRequest::new(row_id, 4096, 1024), + /// ], + /// ) + /// .await?; + /// # let _ = ranges; + /// # Ok(()) + /// # } + /// ``` + /// + /// Returns an error when a range is invalid, a requested row id does not + /// exist, or the column is not a blob v2 column. Returns + /// [`Error::NotSupported`] on table types without blob support. + pub async fn fetch_blob_ranges( + &self, + column: impl AsRef, + requests: impl IntoIterator, + ) -> Result { + let requests = requests.into_iter().collect::>(); + self.inner + .fetch_blob_ranges(column.as_ref(), &requests) + .await + } + /// Open lazy [`BlobFile`] handles for the given row ids. /// /// Same length and order as `row_ids`. Null rows are `None`. Bytes are not @@ -3070,6 +3123,15 @@ impl BaseTable for NativeTable { crate::blob::take_blobs_aligned(&dataset, column, row_ids).await } + async fn fetch_blob_ranges( + &self, + column: &str, + requests: &[BlobRangeRequest], + ) -> Result { + let dataset = self.dataset.get().await?; + crate::blob::take_blob_ranges_aligned(&dataset, column, requests).await + } + async fn fetch_blob_files( &self, column: &str, diff --git a/rust/lancedb/tests/blob_integration.rs b/rust/lancedb/tests/blob_integration.rs index ad0c8d9ff..47b80b433 100644 --- a/rust/lancedb/tests/blob_integration.rs +++ b/rust/lancedb/tests/blob_integration.rs @@ -12,7 +12,7 @@ use futures::TryStreamExt; use lance_encoding::version::LanceFileVersion; use lancedb::{ Connection, Error, Result, Table, - blob::blob, + blob::{BlobRangeRequest, blob}, connect, connect_namespace, database::listing::OPT_NEW_TABLE_ENABLE_STABLE_ROW_IDS, query::{ExecutableQuery, QueryBase}, @@ -595,6 +595,73 @@ async fn fetch_blobs_aligns_with_reordered_and_duplicate_ids() -> Result<()> { Ok(()) } +#[tokio::test] +async fn fetch_blob_ranges_aligns_repeated_ranges_and_nulls() -> Result<()> { + let tmp = tempdir().unwrap(); + let db = connect(tmp.path().to_str().unwrap()).execute().await?; + let table = + create_inline_blob_table(&db, "t", &[1, 2], &[Some(b"abcdefghij".as_slice()), None]) + .await?; + + let pairs = collect_id_rowid(&table).await?; + let by_id = |want: i64| pairs.iter().find(|(id, _)| *id == want).unwrap().1; + let requests = [ + BlobRangeRequest::new(by_id(1), 2, 3), + BlobRangeRequest::new(by_id(2), 0, 0), + BlobRangeRequest::new(by_id(1), 0, 2), + BlobRangeRequest::new(by_id(1), 2, 3), + BlobRangeRequest::new(by_id(1), 10, 0), + ]; + let bytes = table.fetch_blob_ranges("image", requests).await?; + + assert_eq!(bytes.len(), requests.len()); + assert_eq!(bytes.value(0), b"cde"); + assert!(bytes.is_null(1)); + assert_eq!(bytes.value(2), b"ab"); + assert_eq!(bytes.value(3), b"cde"); + assert_eq!(bytes.value(4), b""); + Ok(()) +} + +#[tokio::test] +async fn fetch_blob_ranges_validates_requests() -> Result<()> { + let tmp = tempdir().unwrap(); + let db = connect(tmp.path().to_str().unwrap()).execute().await?; + let table = create_inline_blob_table(&db, "t", &[1], &[Some(b"abc".as_slice())]).await?; + let row_id = collect_row_ids(&table).await?[0]; + + let err = table + .fetch_blob_ranges("image", [BlobRangeRequest::new(row_id, 2, 2)]) + .await + .unwrap_err(); + assert!(err.to_string().contains("exceeds blob size")); + + let err = table + .fetch_blob_ranges("image", [BlobRangeRequest::new(row_id, u64::MAX, 1)]) + .await + .unwrap_err(); + assert!(err.to_string().contains("offset + length overflowed")); + + let err = table + .fetch_blob_ranges("image", [BlobRangeRequest::new(u64::MAX, 0, 1)]) + .await + .unwrap_err(); + assert!(matches!(&err, Error::InvalidInput { .. }), "got {err:?}"); + assert!(err.to_string().contains("row ids")); + Ok(()) +} + +#[tokio::test] +async fn fetch_blob_ranges_empty_requests_returns_empty_array() -> Result<()> { + let tmp = tempdir().unwrap(); + let db = connect(tmp.path().to_str().unwrap()).execute().await?; + let table = create_inline_blob_table(&db, "t", &[1], &[Some(b"x".as_slice())]).await?; + + let bytes = table.fetch_blob_ranges("image", std::iter::empty()).await?; + assert!(bytes.is_empty()); + Ok(()) +} + #[tokio::test] async fn fetch_blobs_empty_ids_returns_empty() -> Result<()> { let tmp = tempdir().unwrap(); @@ -617,6 +684,32 @@ async fn fetch_blobs_out_of_range_id_errors_without_panic() -> Result<()> { Ok(()) } +#[tokio::test] +async fn fetch_blob_apis_reject_mixed_valid_and_missing_row_ids() -> Result<()> { + let tmp = tempdir().unwrap(); + let db = connect(tmp.path().to_str().unwrap()).execute().await?; + let table = create_inline_blob_table(&db, "t", &[1], &[Some(b"x".as_slice())]).await?; + let row_id = collect_row_ids(&table).await?[0]; + let row_ids = [u64::MAX, row_id]; + + let err = table.fetch_blobs("image", &row_ids).await.unwrap_err(); + assert!(matches!(&err, Error::InvalidInput { .. }), "got {err:?}"); + assert!(err.to_string().contains("row ids")); + + let err = table.fetch_blob_files("image", &row_ids).await.unwrap_err(); + assert!(matches!(&err, Error::InvalidInput { .. }), "got {err:?}"); + assert!(err.to_string().contains("row ids")); + + let requests = row_ids.map(|row_id| BlobRangeRequest::new(row_id, 0, 1)); + let err = table + .fetch_blob_ranges("image", requests) + .await + .unwrap_err(); + assert!(matches!(&err, Error::InvalidInput { .. }), "got {err:?}"); + assert!(err.to_string().contains("row ids")); + Ok(()) +} + #[tokio::test] async fn fetch_blobs_rejects_non_blob_column() -> Result<()> { let tmp = tempdir().unwrap(); @@ -843,11 +936,24 @@ async fn fetch_blobs_with_precompaction_row_ids_survives_compaction() -> Result< _ => unreachable!(), } } + + let ranges = ids_before + .iter() + .map(|row_id| BlobRangeRequest::new(*row_id, 5, 3)); + let ranges_after = table.fetch_blob_ranges("image", ranges).await?; + assert_eq!(ranges_after.len(), 2); + for (i, (id, _)) in pairs_before.iter().enumerate() { + match id { + 1 => assert_eq!(ranges_after.value(i), b"one"), + 2 => assert_eq!(ranges_after.value(i), b"two"), + _ => unreachable!(), + } + } Ok(()) } #[tokio::test] -async fn zero_length_blob_reads_back_as_null() -> Result<()> { +async fn empty_blob_reads_back_as_empty_bytes() -> Result<()> { let tmp = tempdir().unwrap(); let db = connect(tmp.path().to_str().unwrap()).execute().await?; let table = create_inline_blob_table(&db, "t", &[1], &[Some(b"".as_slice())]).await?; @@ -855,7 +961,8 @@ async fn zero_length_blob_reads_back_as_null() -> Result<()> { let ids = collect_row_ids(&table).await?; let bytes = table.fetch_blobs("image", &ids).await?; assert_eq!(bytes.len(), 1); - assert!(bytes.is_null(0)); + assert!(!bytes.is_null(0)); + assert!(bytes.value(0).is_empty()); Ok(()) } @@ -927,6 +1034,27 @@ async fn fetch_blobs_aligns_across_fragments_with_nulls_and_dups() -> Result<()> Ok(()) } +#[tokio::test] +async fn fetch_blob_ranges_aligns_across_fragments_with_nulls_and_dups() -> Result<()> { + let tmp = tempdir().unwrap(); + let db = connect(tmp.path().to_str().unwrap()).execute().await?; + let table = multi_fragment_dedicated_blob_table(&db).await?; + let row_ids = row_ids_for_logical(&table, &SCRAMBLED_LOGICAL_IDS).await?; + let requests = row_ids + .iter() + .map(|row_id| BlobRangeRequest::new(*row_id, 123, 8)); + + let bytes = table.fetch_blob_ranges("image", requests).await?; + assert_eq!(bytes.len(), SCRAMBLED_LOGICAL_IDS.len()); + for (slot, logical_id) in SCRAMBLED_LOGICAL_IDS.iter().enumerate() { + match logical_id { + 3 | 5 => assert!(bytes.is_null(slot)), + id => assert_eq!(bytes.value(slot), [*id as u8; 8]), + } + } + Ok(()) +} + #[tokio::test] async fn fetch_blob_files_aligns_across_fragments_with_nulls_and_dups() -> Result<()> { let tmp = tempdir().unwrap(); From 29c030f86587adbe2307ea474e382f465a317c80 Mon Sep 17 00:00:00 2001 From: Dan Tasse <105866+dantasse@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:44:49 -0400 Subject: [PATCH 06/29] fix: accept either timestamp or timestamp_millis for versions (#3733) `list_versions()` against a remote table on a server that uses lance-namespace was failing. The server was returning `timestamp_millis`, while db-catalog deployments were returning `timestamp`, and the client was only accepting `timestamp`. So, updated the client to accept both. (assuming we're migrating over time; eventually we can turn off the `timestamp` code path I suppose.) --- rust/lancedb/src/remote/table.rs | 94 +++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index b17e6c2bb..4c3c1bc6f 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -1631,9 +1631,21 @@ impl BaseTable for RemoteTable { let (request_id, response) = self.send(request, true).await?; let response = self.check_table_response(&request_id, response).await?; + // Servers report the creation time either as an RFC 3339 `timestamp` + // (direct-table path) or as `timestamp_millis` in milliseconds since + // epoch (namespace-backed path), and may omit `metadata`. + #[derive(Deserialize)] + struct VersionEntry { + version: u64, + timestamp: Option>, + timestamp_millis: Option, + #[serde(default)] + metadata: std::collections::BTreeMap, + } + #[derive(Deserialize)] struct ListVersionsResponse { - versions: Vec, + versions: Vec, } let body = response.text().await.err_to_http(request_id.clone())?; @@ -1644,11 +1656,37 @@ impl BaseTable for RemoteTable { err, body ) .into(), - request_id, + request_id: request_id.clone(), status_code: None, })?; - Ok(body.versions) + body.versions + .into_iter() + .map(|entry| { + let timestamp = entry + .timestamp + .or_else(|| { + entry + .timestamp_millis + .and_then(DateTime::::from_timestamp_millis) + }) + .ok_or_else(|| Error::Http { + source: format!( + "list_versions response for version {} has neither a valid \ + `timestamp` nor `timestamp_millis` field", + entry.version + ) + .into(), + request_id: request_id.clone(), + status_code: None, + })?; + Ok(Version { + version: entry.version, + timestamp, + metadata: entry.metadata, + }) + }) + .collect() } async fn schema(&self) -> Result { @@ -5246,6 +5284,56 @@ mod tests { // assert_eq!(versions, expected); } + /// Namespace-backed servers report `timestamp_millis` instead of + /// `timestamp`, and may omit `metadata` entirely. + #[tokio::test] + async fn test_list_versions_timestamp_millis() { + let table = Table::new_with_handler("my_table", |request| { + assert_eq!(request.method(), "POST"); + assert_eq!(request.url().path(), "/v1/table/my_table/version/list/"); + + let response_body = serde_json::json!({ + "versions": [ + { + "version": 1, + "manifest_path": "path/to/_versions/1.manifest", + "timestamp_millis": 1704067200000i64, + }, + { + "version": 2, + "manifest_path": "path/to/_versions/2.manifest", + "timestamp_millis": 1706745600000i64, + "metadata": {"key": "value"}, + }, + ] + }); + let response_body = serde_json::to_string(&response_body).unwrap(); + + http::Response::builder() + .status(200) + .body(response_body) + .unwrap() + }); + + let versions = table.list_versions().await.unwrap(); + assert_eq!(versions.len(), 2); + assert_eq!(versions[0].version, 1); + assert_eq!( + versions[0].timestamp, + "2024-01-01T00:00:00Z".parse::>().unwrap() + ); + assert!(versions[0].metadata.is_empty()); + assert_eq!(versions[1].version, 2); + assert_eq!( + versions[1].timestamp, + "2024-02-01T00:00:00Z".parse::>().unwrap() + ); + assert_eq!( + versions[1].metadata.get("key").map(String::as_str), + Some("value") + ); + } + #[tokio::test] async fn test_index_stats() { let table = Table::new_with_handler("my_table", |request| { From 1ebde1f06c4b1fe6b061d87b48809dc0b9302dcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:28:54 -0700 Subject: [PATCH 07/29] chore(deps): bump arrow from 58.3.0 to 58.4.0 (#3722) Bumps [arrow](https://github.com/apache/arrow-rs) from 58.3.0 to 58.4.0.
Release notes

Sourced from arrow's releases.

arrow 58.4.0

Changelog

58.4.0 (2026-07-17)

Full Changelog

Merged pull requests:

* This Changelog was automatically generated by github_changelog_generator

Changelog

Sourced from arrow's changelog.

58.4.0 (2026-07-17)

Full Changelog

Merged pull requests:

* This Changelog was automatically generated by github_changelog_generator

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 60 +++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e80165a70..a105a0da3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,9 +217,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378530e55cd479eda3c14eb345310799717e6f76d0c332041e8487022166b471" +checksum = "6cfdd0833e32a9874d2b55089333ad310c0be208aafa277385ce2461dec90be3" dependencies = [ "arrow-arith", "arrow-array", @@ -239,9 +239,9 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ab212d2c1886e802f51c5212d78ebbcbb0bec980fff9dadc1eb8d45cd0b738" +checksum = "0a41203398f0eaa6f7ec8e62c0da742a21abf282c148fc157f6c35c90e29981a" dependencies = [ "arrow-array", "arrow-buffer", @@ -253,9 +253,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd33d3e92f207444098c75b42de99d329562be0cf686b307b097cc52b4e999e" +checksum = "ae33dad492b7df00a217563a7b0ef2874df68a0deea1b1a3acf628152f7f7a69" dependencies = [ "ahash", "arrow-buffer", @@ -272,9 +272,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6cd424c2693bcdbc150d843dc9d4d137dd2de4782ce6df491ad11a3a0416c0" +checksum = "b9552f96391c005e6ab449fa941420935e7e062489b12b8b1b08879b2163f5b5" dependencies = [ "bytes", "half", @@ -284,9 +284,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c5aefb56a2c02e9e2b30746241058b85f8983f0fcff2ba0c6d09006e1cded7f" +checksum = "3a8a327c9649f30d8406995f27642b68df354713cca3baaaf100f076f18d5f34" dependencies = [ "arrow-array", "arrow-buffer", @@ -306,9 +306,9 @@ dependencies = [ [[package]] name = "arrow-csv" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94e8cf7e517657a52b91ea1263acf38c4ca62a84655d72458a3359b12ab97de" +checksum = "af0dd6d90d1955e9f9a014c1e563ee8aeffc21909085d25623e1da44d96eca26" dependencies = [ "arrow-array", "arrow-cast", @@ -321,9 +321,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c88210023a2bfee1896af366309a3028fc3bcbd6515fa29a7990ee1baa08ee0" +checksum = "2b24852db04738907e06c04ea61e42fe7fda962a34513022dc0d0e754fb7976b" dependencies = [ "arrow-buffer", "arrow-schema", @@ -334,9 +334,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238438f0834483703d88896db6fe5a7138b2230debc31b34c0336c2996e3c64f" +checksum = "29a908a11fcfb3fb2f6730f4ac15e367bc644e419155e96238f68cf3adde572b" dependencies = [ "arrow-array", "arrow-buffer", @@ -350,9 +350,9 @@ dependencies = [ [[package]] name = "arrow-json" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "205ca2119e6d679d5c133c6f30e68f027738d95ed948cf77677ea69c7800036b" +checksum = "b8a96aed3931c076adee39ec2a40d8219fc7f09e79bcdaca1df16272993e1e14" dependencies = [ "arrow-array", "arrow-buffer", @@ -375,9 +375,9 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bffd8fd2579286a5d63bac898159873e5094a79009940bcb42bbfce4f19f1d0" +checksum = "63a083ec750f5c043f02946b4baf05fcdbb55f4560a3277055caca5cc99f3eb0" dependencies = [ "arrow-array", "arrow-buffer", @@ -388,9 +388,9 @@ dependencies = [ [[package]] name = "arrow-pyarrow" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29abdf672a81c1aeb57fd2661457f9918964d49aed0e9f18932535f2a9e49ce" +checksum = "3ffb9be5a873590f825aef50df20e0f8dff5fd42a77058e28bbe7bd44bb53dec" dependencies = [ "arrow-array", "arrow-data", @@ -400,9 +400,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab5994731204603c73ba69267616c50f80780774c6bb0476f1f830625115e0c" +checksum = "514ba0ef0d4c5896202dae736251ce415abb43a950bed570fb7981b8716c0e4c" dependencies = [ "arrow-array", "arrow-buffer", @@ -413,9 +413,9 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f633dbfdf39c039ada1bf9e34c694816eb71fbb7dc78f613993b7245e078a1ed" +checksum = "21ca356ad6425cecb6eb7b28e4f659f1ee7880fbb1a16127de7dd62901efee9e" dependencies = [ "bitflags 2.11.1", "serde_core", @@ -424,9 +424,9 @@ dependencies = [ [[package]] name = "arrow-select" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd065c54172ac787cf3f2f8d4107e0d3fdc26edba76fdf4f4cc170258942222" +checksum = "c58da39eb3d8350ad4a549e5c2bc49284dac554016c69829310350f1731b0aad" dependencies = [ "ahash", "arrow-array", @@ -438,9 +438,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "58.3.0" +version = "58.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29dd7cda3ab9692f43a2e4acc444d760cc17b12bb6d8232ddf64e9bab7c06b42" +checksum = "b6789b388467525e3271326b6b4915666ecfdf5142aef09779445c954b67543c" dependencies = [ "arrow-array", "arrow-buffer", From 72fc660f9e33418d2d0ec93a5cbb63ce1e96f02b Mon Sep 17 00:00:00 2001 From: kid <19265318+u70b3@users.noreply.github.com> Date: Wed, 29 Jul 2026 04:31:06 +0800 Subject: [PATCH 08/29] feat(python): expose AsyncTable.to_lance (#3730) ## Summary - expose the existing async Lance dataset conversion as `AsyncTable.to_lance` - preserve table version, branch, and refreshed storage options when opening the dataset - route internal async pandas/query paths through the public API - cover normal tables, checked-out versions, branches, and forwarded dataset options ## Testing - `cd python && uv run --no-sync pytest python/tests/test_table.py -q` - `cd python && uv run --no-sync pytest python/tests/test_query.py -q` - `cd python && uv run --no-sync pytest --doctest-modules python/lancedb/table.py -q` - `uv run --project python --no-sync ruff format --check python/python/lancedb/table.py python/python/lancedb/query.py python/python/tests/test_table.py` - `uv run --project python --no-sync ruff check .` Fixes #1387 --- python/python/lancedb/query.py | 2 +- python/python/lancedb/table.py | 21 ++++++++++++-- python/python/tests/test_table.py | 47 +++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/python/python/lancedb/query.py b/python/python/lancedb/query.py index 7063f08cc..b85f50d7b 100644 --- a/python/python/lancedb/query.py +++ b/python/python/lancedb/query.py @@ -3041,7 +3041,7 @@ class AsyncQueryBase(object): if blob_mode == "bytes" else {} ) - dataset = await self._table._to_lance() + dataset = await self._table.to_lance() scanner = dataset.scanner( **_scanner_kwargs_for_query( query, diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index 6c3854734..1da366a78 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -4678,7 +4678,24 @@ class AsyncTable: """ return AsyncQuery(self._inner.query(), self) - async def _to_lance(self, **kwargs) -> lance.LanceDataset: + async def to_lance(self, **kwargs) -> lance.LanceDataset: + """Return the Lance dataset backing this table. + + Parameters + ---------- + **kwargs + Forwarded to [`lance.dataset`][lance.dataset]. + + Returns + ------- + lance.LanceDataset + The Lance dataset at this table handle's version and branch. + + Examples + -------- + >>> async def get_lance_dataset(table): + ... return await table.to_lance() + """ try: import lance except ImportError: @@ -4728,7 +4745,7 @@ class AsyncTable: return (await self.to_arrow()).to_pandas(**kwargs) if blob_mode == "bytes" and blob_v2_column_paths(schema): return await self.query().to_pandas(blob_mode=blob_mode, **kwargs) - return (await self._to_lance()).to_pandas(blob_mode=blob_mode, **kwargs) + return (await self.to_lance()).to_pandas(blob_mode=blob_mode, **kwargs) async def to_arrow(self) -> pa.Table: """Return the table as a pyarrow Table. diff --git a/python/python/tests/test_table.py b/python/python/tests/test_table.py index 456b900ee..b281e74da 100644 --- a/python/python/tests/test_table.py +++ b/python/python/tests/test_table.py @@ -1257,6 +1257,53 @@ def test_branch_to_lance_targets_branch(tmp_path): assert table.to_lance().count_rows() == 1 +@pytest.mark.asyncio +async def test_async_to_lance(tmp_path): + pytest.importorskip("lance") + db = await lancedb.connect_async(tmp_path) + table = await db.create_table("t", [{"i": 1}]) + + dataset = await table.to_lance() + + assert dataset.count_rows() == 1 + + +@pytest.mark.asyncio +async def test_async_branch_to_lance_targets_branch(tmp_path): + pytest.importorskip("lance") + db = await lancedb.connect_async(tmp_path) + table = await db.create_table("t", [{"i": 1}]) + branch = await table.branches.create("exp") + await branch.add([{"i": 2}]) + + assert (await branch.to_lance()).count_rows() == 2 + assert (await table.to_lance()).count_rows() == 1 + + +@pytest.mark.asyncio +async def test_async_to_lance_targets_checked_out_version(tmp_path): + pytest.importorskip("lance") + db = await lancedb.connect_async(tmp_path) + table = await db.create_table("t", [{"i": 1}]) + version = await table.version() + await table.add([{"i": 2}]) + checked_out = await db.open_table("t", version=version) + + assert (await checked_out.to_lance()).count_rows() == 1 + assert (await table.to_lance()).count_rows() == 2 + + +@pytest.mark.asyncio +async def test_async_to_lance_forwards_dataset_options(tmp_path): + pytest.importorskip("lance") + db = await lancedb.connect_async(tmp_path) + table = await db.create_table("t", [{"i": 1}]) + + dataset = await table.to_lance(default_scan_options={"with_row_id": True}) + + assert "_rowid" in dataset.schema.names + + @pytest.mark.asyncio async def test_async_branches(tmp_path): db = await lancedb.connect_async(tmp_path) From b799ebaa6910d1d410d33ec70c653ae2827cc1a9 Mon Sep 17 00:00:00 2001 From: kid <19265318+u70b3@users.noreply.github.com> Date: Wed, 29 Jul 2026 04:36:08 +0800 Subject: [PATCH 09/29] fix(node): reject non-string Arrow metadata (#3728) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - validate Arrow metadata keys and values independently at runtime - reject malformed foreign schemas before constructing a local Arrow schema - cover valid and invalid metadata entries across Arrow 15–18 ## Testing - `node_modules/.bin/jest --runInBand __test__/arrow.test.ts -t "schema metadata"` - `node_modules/.bin/jest --runInBand __test__/arrow.test.ts` - `node node_modules/@biomejs/biome/bin/biome format --write lancedb/sanitize.ts __test__/arrow.test.ts` - `pnpm lint` - `pnpm build` - `pnpm run docs` Fixes #3729 --- nodejs/__test__/arrow.test.ts | 31 +++++++++++++++++++++++++++++++ nodejs/lancedb/sanitize.ts | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/nodejs/__test__/arrow.test.ts b/nodejs/__test__/arrow.test.ts index 2735f7f72..9e20e3c04 100644 --- a/nodejs/__test__/arrow.test.ts +++ b/nodejs/__test__/arrow.test.ts @@ -991,6 +991,37 @@ describe.each([arrow15, arrow16, arrow17, arrow18])( expectValidMapField(roundTripped.schema.fields[0]); }); + + it("preserves string schema metadata", function () { + const metadata = new Map([["source", "fixture"]]); + const schema = new Schema( + [new Field("value", new Int32(), true)], + metadata, + ); + + expect(makeEmptyTable(schema).schema.metadata.get("source")).toBe( + "fixture", + ); + }); + + it.each([ + ["non-string keys", new Map([[42, "fixture"]])], + ["non-string values", new Map([["source", 42]])], + [ + "non-string keys and values", + new Map([[42, false]]), + ], + ])("rejects schema metadata with %s", function (_, metadataLike) { + const metadata = metadataLike as unknown as Map; + const schema = new Schema( + [new Field("value", new Int32(), true)], + metadata, + ); + + expect(() => makeEmptyTable(schema)).toThrow( + "Expected metadata, if present, to be a Map but it had non-string keys or values", + ); + }); }); describe("when using two versions of arrow", function () { diff --git a/nodejs/lancedb/sanitize.ts b/nodejs/lancedb/sanitize.ts index 9f834a7f6..ae0bc0179 100644 --- a/nodejs/lancedb/sanitize.ts +++ b/nodejs/lancedb/sanitize.ts @@ -84,7 +84,7 @@ export function sanitizeMetadata( throw Error("Expected metadata, if present, to be a Map"); } for (const item of metadataLike) { - if (!(typeof item[0] === "string" || !(typeof item[1] === "string"))) { + if (typeof item[0] !== "string" || typeof item[1] !== "string") { throw Error( "Expected metadata, if present, to be a Map but it had non-string keys or values", ); From ff50e698cfb5b7f16a0ba11619cd306dca6b9acf Mon Sep 17 00:00:00 2001 From: Will Jones Date: Tue, 28 Jul 2026 14:38:21 -0700 Subject: [PATCH 10/29] ci: cut Actions cost by moving builds to free runners and fixing caches (#3735) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standard GitHub-hosted runners are free on public repos, so all Actions spend here is on the `*-8x-*` / `4x` larger runners. Measured over 30 days at current (post-Jan-2026) larger-runner rates, that is ~$1,400/mo, and `npm-publish` is ~70% of it. ## Changes **Fat LTO was forcing builds onto large runners.** `[profile.release]` in `.cargo/config.toml` sets `lto = "fat"` with `codegen-units = 1`, which is single-threaded and the peak-memory step. The macOS `npm-publish` build was 111 of its 113 minutes in one `napi build` step, making it the critical path of the whole publish pipeline. The ThinLTO override already applied to Windows now covers macOS too, and both Windows builds move from `windows-2025-8x-x64` to the free standard `windows-2025`. **The npm-publish cargo cache never existed.** There are zero caches with its key prefix. The key was static, so `actions/cache` (which only writes on a miss) could never refresh it, and a multi-GB release `target/` per target could never fit the repo's 10 GB budget anyway. Now caches only the crate registry, keyed on `Cargo.lock`. The docker builds also mounted `.cargo/registry/*` while the cache saved `.cargo-cache`, so containers re-downloaded the registry every run. **Cache eviction thrash.** Repo cache usage is 10.4 GB against GitHub's 10 GB cap, so every PR run evicted main's warm entries. `rust.yml` and `nodejs.yml` now restore everywhere but only save from `main`. **npm-publish moves to nightly + tags** instead of every push to main (~90/month). The cross-compiled targets do need watching, so `report-failure` now fires on scheduled runs, and dedupes onto an existing open issue rather than filing one per night. **rust.yml aarch64-pc-windows-msvc** cross-compiled its tests and then skipped them, paying full codegen and link cost for a compile check. `windows-11-arm` is now GA and free on public repos, so it builds and tests natively. Its test step also passes `--target` — without it cargo used `target/ci/` rather than `target//ci/` and rebuilt the entire dependency graph a second time. **pypi-publish.yml had no concurrency group**, so force-pushes left a ~74 minute Windows job running. ## What is cost vs. wall-clock | Change | Cost | Wall-clock | |---|---|---| | Windows npm-publish → free runners | **−$570/mo** | slower per job (8→4 cores) | | npm-publish nightly | **−$125/mo** | — | | pypi-publish concurrency | small | — | | macOS ThinLTO | $0 (already free) | **−~50 min** per release | | rust aarch64 Windows native | $0 (already free) | **−~25 min** | | rust `--target` on test step | $0 | large, avoids a second full build | | rust-cache `save-if` | small | faster via real cache hits | ## Risks - The two Windows builds now have 4 cores instead of 8 and ~14 GB of free disk. If they fail, it is most likely disk rather than memory; fallback is `windows-2025-4x-x64`, which still halves that line. - `windows-11-arm` has a thinner toolset (choco/vcpkg/protoc under emulation) and this enables a test step that has never run, so it may surface real aarch64 failures. That is the point, but it is the change most likely to need iteration. - ThinLTO applies to published macOS and Windows binaries, typically within a few percent of fat LTO. Linux release builds are untouched. ## Follow-ups - `python.yml` `pydantic1x` (37 min) and `Doctest` (33 min) each rebuild the extension from source via `pip install -e .` with no Rust cache; they should consume the wheel the `linux` job already builds. Worth ~$235/mo and ~70 min of compute per run. Separate PR. - The three `ubuntu-2404-8x-x64` npm-publish builds (~$420/mo at the old cadence) are the remaining large-runner spend; `aarch64-unknown-linux-gnu` could run natively on free `ubuntu-24.04-arm`. Worth doing after this lands so the ThinLTO change can be validated first. - The wheel composite actions declare `python-minor-version` as required but never use it, and every caller omits it (actionlint warns). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) --- .../actions/create-failure-issue/action.yml | 24 +++- .github/workflows/nodejs.yml | 15 +++ .github/workflows/npm-publish.yml | 117 ++++++++++++++---- .github/workflows/pypi-publish.yml | 13 ++ .github/workflows/python.yml | 33 +++++ .github/workflows/rust.yml | 52 ++++++-- 6 files changed, 219 insertions(+), 35 deletions(-) diff --git a/.github/actions/create-failure-issue/action.yml b/.github/actions/create-failure-issue/action.yml index 832dc8def..33e3b80ae 100644 --- a/.github/actions/create-failure-issue/action.yml +++ b/.github/actions/create-failure-issue/action.yml @@ -27,19 +27,31 @@ runs: # Extract failed job names FAILED_JOBS=$(echo "$JOB_RESULTS" | jq -r 'to_entries | map(select(.value.result == "failure")) | map(.key) | join(", ")') - # Create issue with workflow name, failed jobs, and run URL - gh issue create \ - --title "$WORKFLOW_NAME Failed ($FAILED_JOBS)" \ - --body "The workflow **$WORKFLOW_NAME** failed during execution. + TITLE="$WORKFLOW_NAME Failed ($FAILED_JOBS)" + + # This action now also runs on nightly schedules, so a breakage that + # persists for a few days would otherwise file one issue per night. + # Comment on the open report instead when one already exists. + EXISTING=$(gh issue list --state open --label ci --limit 100 --json number,title \ + | jq -r --arg title "$TITLE" 'map(select(.title == $title)) | .[0].number // empty') + + if [ -n "$EXISTING" ]; then + gh issue comment "$EXISTING" --body "Failed again: $RUN_URL" + echo "Commented on existing issue #$EXISTING" + else + gh issue create \ + --title "$TITLE" \ + --body "The workflow **$WORKFLOW_NAME** failed during execution. **Failed jobs:** $FAILED_JOBS **Run URL:** $RUN_URL Please investigate the failed jobs and address any issues." \ - --label "ci" + --label "ci" - echo "Issue created successfully" + echo "Issue created successfully" + fi else echo "No job failures detected, skipping issue creation" fi diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 3452168a5..e82cf71c9 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -61,6 +61,11 @@ jobs: sudo apt update sudo apt install -y protobuf-compiler libssl-dev - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Format Rust run: cargo fmt --all -- --check - name: Lint Rust @@ -103,6 +108,11 @@ jobs: cache: 'pnpm' cache-dependency-path: nodejs/pnpm-lock.yaml - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install dependencies run: | sudo apt update @@ -182,6 +192,11 @@ jobs: cache-dependency-path: nodejs/pnpm-lock.yaml - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install dependencies run: | brew install protobuf diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 8a049ee0f..72ef5ad13 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -10,10 +10,16 @@ permissions: on: push: - branches: - - main tags: - "v*" + # The cross-compiled targets (musl especially) break from toolchain and + # dependency changes that nothing else in CI catches, and discovering that + # mid-release is expensive. A nightly run keeps that signal while dropping + # the full 8-target release matrix from all ~90 pushes to main each month. + # `report-failure` files an issue when a nightly breaks. + schedule: + - cron: "0 8 * * *" + workflow_dispatch: pull_request: # This should trigger a dry run (we skip the final publish step) paths: @@ -34,9 +40,18 @@ jobs: - target: aarch64-apple-darwin host: macos-latest features: fp16kernels - pre_build: brew install protobuf + pre_build: |- + brew install protobuf + # Fat LTO (the workspace default in .cargo/config.toml) is + # single-threaded and is the peak-memory step of the build. On + # this runner it accounted for ~111 of the job's ~113 minutes, + # making it the critical path of the entire publish pipeline. + # ThinLTO parallelizes it across the runner's cores, for a few + # percent of runtime performance. + export CARGO_PROFILE_RELEASE_LTO=thin + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 - target: x86_64-pc-windows-msvc - host: windows-2025-8x-x64 + host: windows-2025 features: "," pre_build: |- choco install --no-progress protoc ninja nasm @@ -44,19 +59,19 @@ jobs: # There is an issue where choco doesn't add nasm to the path export PATH="$PATH:/c/Program Files/NASM" nasm -v - # Fat LTO of the cdylib is single-threaded and the peak-memory - # step of the build, and had started hitting rustc-LLVM OOM on the - # Windows runners. ThinLTO parallelizes it across the runner's - # cores and keeps peak memory well under the limit. + # See the ThinLTO note on aarch64-apple-darwin above. Keeping + # peak memory down is also what lets this run on the standard + # 4-core runner: the 8-core larger runner was only needed to + # stop fat LTO from OOMing rustc-LLVM. export CARGO_PROFILE_RELEASE_LTO=thin export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 - target: aarch64-pc-windows-msvc - host: windows-2025-8x-x64 + host: windows-2025 features: "," pre_build: |- choco install --no-progress protoc rustup target add aarch64-pc-windows-msvc - # See ThinLTO note on the x86_64-pc-windows-msvc target above. + # See the ThinLTO note on aarch64-apple-darwin above. export CARGO_PROFILE_RELEASE_LTO=thin export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 - target: x86_64-unknown-linux-gnu @@ -131,16 +146,49 @@ jobs: with: toolchain: stable targets: ${{ matrix.settings.target }} - - name: Cache cargo - uses: actions/cache@v5 + # These builds were entirely uncached: the old key was static, so + # `actions/cache` (which only writes on a miss) could never refresh it, + # and the multi-GB whole-`target/` copy it tried to store never fit the + # repo's cache budget, so no entry was ever saved. rust-cache prunes + # `target/` to dependency artifacts and keys on Cargo.lock plus the rustc + # version, which both fixes the key and keeps entries a sane size. + # + # This caches dependency *compilation* only. The LTO link of the cdylib + # re-runs regardless, since the local crate changes every time, so the + # win is larger on the non-LTO jobs than here. + - name: Cache cargo (native builds) + uses: Swatinem/rust-cache@v2 + if: ${{ !matrix.settings.docker }} with: - path: | - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - .cargo-cache - target/ - key: nodejs-${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} + # The release profile and per-target dirs differ from what the test + # workflows cache, so these need to be separate entries. + key: release-${{ matrix.settings.target }} + # Only the nightly run on main writes, so tag and PR runs restore a + # warm entry without every dependabot PR writing its own (which would + # be unreadable elsewhere anyway, since GitHub scopes caches to the + # creating ref). The nightly cadence also keeps entries inside + # GitHub's 7-day eviction window, which a tag-only trigger would not. + save-if: ${{ github.ref == 'refs/heads/main' }} + # Docker builds can use rust-cache too. `target/` already lives on the + # host because the whole workspace is bind-mounted into the container, and + # rust-cache's prune and save run host-side, so they can manage it -- which + # is what keeps the entry to dependency artifacts rather than a multi-GB + # copy of everything. + # + # Two differences from the native builds. The container's CARGO_HOME is + # bind-mounted from `.cargo-cache` rather than the host's ~/.cargo, so that + # has to be cached explicitly. And the key is derived from the *host* rustc + # version, which is not the compiler that produced these artifacts; that is + # safe because cargo fingerprints the real compiler and rebuilds on a + # mismatch, it just means a base-image toolchain bump costs one cold build + # instead of invalidating the key. + - name: Cache cargo (docker builds) + uses: Swatinem/rust-cache@v2 + if: ${{ matrix.settings.docker }} + with: + key: docker-${{ matrix.settings.target }} + cache-directories: .cargo-cache + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install dependencies run: pnpm install --frozen-lockfile - name: Install Zig @@ -158,9 +206,13 @@ jobs: if: ${{ matrix.settings.docker }} with: image: ${{ matrix.settings.docker }} + # All three mounts must live under `.cargo-cache`, which is what the + # cache step above saves. Previously the registry mounts pointed at + # `.cargo/...`, a path nothing cached, so the container re-downloaded + # the whole crate registry on every run. options: "--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db \ - -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache \ - -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index \ + -v ${{ github.workspace }}/.cargo-cache/registry/cache:/usr/local/cargo/registry/cache \ + -v ${{ github.workspace }}/.cargo-cache/registry/index:/usr/local/cargo/registry/index \ -v ${{ github.workspace }}:/build -w /build/nodejs" run: | set -e @@ -172,6 +224,16 @@ jobs: --js ../lancedb/native.js \ --strip \ --output-dir dist/ + # The container runs as root (`--user 0:0`), so everything it wrote to the + # mounted cache dirs is root-owned. rust-cache's post step runs as the + # runner user and has to both read these and delete from them while + # pruning, so hand them back before it runs. + - name: Take ownership of docker build output + if: ${{ matrix.settings.docker }} + run: | + sudo chown -R "$(id -u):$(id -g)" \ + "${{ github.workspace }}/.cargo-cache" \ + "${{ github.workspace }}/target" - name: Build run: | ${{ matrix.settings.pre_build }} @@ -185,6 +247,15 @@ jobs: --output-dir dist/ if: ${{ !matrix.settings.docker }} shell: bash + # The standard Windows runners have ~14 GB free, and a release `target/` + # for this workspace is a large fraction of that. Report the remaining + # headroom so a build that only just fits is visible before a dependency + # bump turns it into a failed release. `always()` so the numbers are + # still there when the build is what ran out of space. + - name: Report disk headroom + if: always() + run: df -h + shell: bash - name: Upload artifact uses: actions/upload-artifact@v7 with: @@ -335,7 +406,9 @@ jobs: name: Report Workflow Failure runs-on: ubuntu-latest needs: [build-lancedb, test-lancedb, publish] - if: always() && failure() && startsWith(github.ref, 'refs/tags/v') + # Nightly runs are the only thing watching the cross-compiled targets now, + # so they have to report failures too or the signal is silently lost. + if: always() && failure() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule') permissions: contents: read issues: write diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 8825e7fb0..74b7d05e6 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -20,6 +20,12 @@ env: permissions: contents: read +# Without this, a force-push to a PR leaves the previous run going -- including +# a ~74 minute Windows job and a billed arm64 wheel build. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: linux: name: Python ${{ matrix.config.package_name }} ${{ matrix.config.platform }} manylinux${{ matrix.config.manylinux }} @@ -122,6 +128,13 @@ jobs: uses: actions/setup-python@v6 with: python-version: "3.13" + # NOTE: caching cargo here would be a no-op. This workflow only runs on + # tags and PRs, and GitHub only lets a run restore caches from its own ref + # or the default branch -- so with no run on main there is nothing that + # can populate an entry the release build would be allowed to read. Fixing + # this needs a main/nightly trigger (which would also catch wheel-build + # breakage before a release); the ~74 minutes here is otherwise dominated + # by the fat-LTO link, which no cache avoids. - uses: ./.github/workflows/build_windows_wheel with: python-minor-version: 10 diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index ee8b319bb..52582395f 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -108,6 +108,15 @@ jobs: run: | sudo apt update sudo apt install -y protobuf-compiler + # `pip install -e .` builds the extension with maturin, which is most of + # this job's ~33 minutes. It had no Rust cache, so every dependency was + # recompiled from scratch on every run. + - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install run: | pip install --extra-index-url https://pypi.fury.io/lance-format/ --extra-index-url https://pypi.fury.io/lancedb/ -e .[tests,dev,embeddings] @@ -168,6 +177,14 @@ jobs: uses: actions/setup-python@v6 with: python-version: "3.13" + # maturin runs cargo natively on macOS (docker is Linux-only), so the host + # target dir is cacheable. This job had no Rust cache. + - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - uses: ./.github/workflows/build_mac_wheel with: args: --profile ci @@ -197,6 +214,14 @@ jobs: uses: actions/setup-python@v6 with: python-version: "3.13" + # maturin runs cargo natively on Windows (docker is Linux-only), so the + # host target dir is cacheable. This job had no Rust cache at all and so + # rebuilt every dependency from scratch on every run. + - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. The repo sits at + # GitHub's cache cap, so per-PR saves just evict main's entries. + save-if: ${{ github.ref == 'refs/heads/main' }} - uses: ./.github/workflows/build_windows_wheel with: args: --profile ci @@ -224,6 +249,14 @@ jobs: uses: actions/setup-python@v6 with: python-version: "3.10" + # As with Doctest, `pip install -e .` compiles the extension and this job + # had no Rust cache, which is most of its ~37 minutes. + - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install lancedb run: | pip install "pydantic<2" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3bc67d2ec..1a0b65c4a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -48,6 +48,11 @@ jobs: with: components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install dependencies run: | sudo apt update @@ -89,6 +94,11 @@ jobs: run: rm -f Cargo.lock - uses: rui314/setup-mold@v1 - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install dependencies run: | sudo apt update @@ -118,6 +128,11 @@ jobs: fetch-depth: 0 lfs: true - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install dependencies run: | sudo apt update @@ -175,6 +190,11 @@ jobs: - name: CPU features run: sysctl -a | grep cpu - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install dependencies run: brew install protobuf - name: Run tests @@ -187,12 +207,19 @@ jobs: cargo test --profile ci --features $ALL_FEATURES --locked windows: - runs-on: windows-2022 strategy: + fail-fast: false matrix: - target: - - x86_64-pc-windows-msvc - - aarch64-pc-windows-msvc + include: + - target: x86_64-pc-windows-msvc + runner: windows-2022 + # windows-11-arm is a standard runner, so it is free on public repos. + # Running natively lets the aarch64 tests actually execute -- this + # job used to cross-compile them and then skip the test step, paying + # full codegen and link cost for a compile check. + - target: aarch64-pc-windows-msvc + runner: windows-11-arm + runs-on: ${{ matrix.runner }} defaults: run: working-directory: rust/lancedb @@ -201,6 +228,11 @@ jobs: - name: Set target run: rustup target add ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Install Protoc v21.12 run: choco install --no-progress protoc - name: Build @@ -208,11 +240,12 @@ jobs: $env:VCPKG_ROOT = $env:VCPKG_INSTALLATION_ROOT cargo build --profile ci --features aws,remote --tests --locked --target ${{ matrix.target }} - name: Run tests - # Can only run tests when target matches host - if: ${{ matrix.target == 'x86_64-pc-windows-msvc' }} run: | $env:VCPKG_ROOT = $env:VCPKG_INSTALLATION_ROOT - cargo test --profile ci --features aws,remote --locked + # `--target` has to match the build step above. Without it cargo uses + # target/ci/ rather than target//ci/ and rebuilds the entire + # dependency graph a second time. + cargo test --profile ci --features aws,remote --locked --target ${{ matrix.target }} msrv: # Check the minimum supported Rust version @@ -238,6 +271,11 @@ jobs: with: toolchain: ${{ matrix.msrv }} - uses: Swatinem/rust-cache@v2 + with: + # Restore everywhere, but only save from main. Per-PR saves are + # unreadable outside their own branch anyway, since GitHub scopes + # caches to the creating ref. + save-if: ${{ github.ref == 'refs/heads/main' }} - name: Downgrade dependencies # These packages have newer requirements for MSRV run: | From 98a52267a2d192e0d5d2d5c62ba874247ddd4896 Mon Sep 17 00:00:00 2001 From: buduoqiu Date: Wed, 29 Jul 2026 06:38:34 +0800 Subject: [PATCH 11/29] feat(python): configure streaming transform parallelism (#3699) ## Summary - add a keyword-only `transform_parallelism` option to `StreamingDataset` - preserve CPU auto-detection by default and fall back to one worker when unavailable - apply the configured limit to both the transform executor and concurrency semaphore - document and test explicit, default, fallback, and invalid values ## Testing - `uv run --extra tests --with torch pytest python/tests/test_elastic_dataloader.py -q` (`136 passed`) - `uvx ruff check python/lancedb/streaming.py python/tests/test_elastic_dataloader.py` - `uvx ruff format --check python/lancedb/streaming.py python/tests/test_elastic_dataloader.py` - `git diff --check origin/main...HEAD` Closes #3695 Co-authored-by: buduoqiu --- python/python/lancedb/streaming.py | 29 ++++++++++----- .../python/tests/test_elastic_dataloader.py | 36 +++++++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/python/python/lancedb/streaming.py b/python/python/lancedb/streaming.py index 9ffc612d9..525ed3d63 100644 --- a/python/python/lancedb/streaming.py +++ b/python/python/lancedb/streaming.py @@ -59,9 +59,10 @@ class StreamingDataset(IterableDataset): - **Stage 1 (I/O)**: one thread pool with ``num_splits * prefetch_batches`` workers fetches raw ``RecordBatch`` objects from LanceDB in parallel across all splits and places them in a per-split raw-batch queue. - - **Stage 2 (transform)**: a second thread pool with ``os.cpu_count()`` - workers picks up raw batches, applies the transform, and places the - results in a per-split cooked-row queue. + - **Stage 2 (transform)**: a second thread pool with + ``transform_parallelism`` workers picks up raw batches, applies the + transform, and places the results in a per-split cooked-row queue. By + default, the number of workers is determined by ``os.cpu_count()``. The main thread round-robins over the cooked queues, yielding one row per split per cycle. @@ -122,6 +123,10 @@ class StreamingDataset(IterableDataset): are yielded. Receives one batch at a time and must return an iterable whose length equals the number of rows in the batch. When ``None`` (the default) rows are returned as plain Python dicts. + transform_parallelism: + Maximum number of transforms to run concurrently. Must be greater + than zero. When ``None`` (the default), uses ``os.cpu_count()`` or 1 + when the CPU count is unavailable. worker_info_override: If set, used in place of ``torch.utils.data.get_worker_info()`` to determine the DataLoader worker assignment. Intended for unit tests @@ -146,6 +151,7 @@ class StreamingDataset(IterableDataset): shuffle_clump_size: Optional[int] = None, filter: Optional[str] = None, transform: Optional[Callable] = None, + transform_parallelism: Optional[int] = None, connection_factory: Optional[Callable[[str], Any]] = None, worker_info_override=None, ): @@ -159,6 +165,8 @@ class StreamingDataset(IterableDataset): f"num_splits ({num_splits}) must be divisible by " f"world_size ({world_size})" ) + if transform_parallelism is not None and transform_parallelism <= 0: + raise ValueError("transform_parallelism must be greater than 0") self._table = table self._num_splits = num_splits @@ -173,6 +181,7 @@ class StreamingDataset(IterableDataset): self._shuffle_clump_size = shuffle_clump_size self._filter = filter self._transform = transform + self._transform_parallelism = transform_parallelism self._connection_factory = connection_factory self._worker_info_override = worker_info_override @@ -284,7 +293,11 @@ class StreamingDataset(IterableDataset): batch_size = self._read_batch_size max_prefetch = self._prefetch_batches - cpu_workers = os.cpu_count() or 1 + transform_workers = ( + self._transform_parallelism + if self._transform_parallelism is not None + else (os.cpu_count() or 1) + ) final_transform = ( self._transform if self._transform is not None else Transforms.arrow2python ) @@ -296,8 +309,8 @@ class StreamingDataset(IterableDataset): tx_pending = [deque() for _ in range(n)] # Future[list[Any]] cooked = [deque() for _ in range(n)] # rows ready to yield - # Limit simultaneous transforms to cpu_workers across all splits. - tx_semaphore = threading.Semaphore(cpu_workers) + # Limit simultaneous transforms to transform_workers across all splits. + tx_semaphore = threading.Semaphore(transform_workers) # ── Stage 1 helpers ─────────────────────────────────────────────────── @@ -369,7 +382,7 @@ class StreamingDataset(IterableDataset): _advance(i) elif raw_batches[i]: # Acquire a transform slot (may block briefly if all - # cpu_workers are busy with other splits). + # transform_workers are busy with other splits). tx_semaphore.acquire() batch = raw_batches[i].popleft() tx_pending[i].append(tx_pool.submit(_tx_call_guarded, batch)) @@ -383,7 +396,7 @@ class StreamingDataset(IterableDataset): # ── Main loop ───────────────────────────────────────────────────────── with ThreadPoolExecutor(max_workers=n * max_prefetch) as io_pool: - with ThreadPoolExecutor(max_workers=cpu_workers) as tx_pool: + with ThreadPoolExecutor(max_workers=transform_workers) as tx_pool: self._raw_batches_ref = raw_batches self._cooked_ref = cooked self._fetch_head_ref = fetch_head diff --git a/python/python/tests/test_elastic_dataloader.py b/python/python/tests/test_elastic_dataloader.py index 6a98bed0c..22918082b 100644 --- a/python/python/tests/test_elastic_dataloader.py +++ b/python/python/tests/test_elastic_dataloader.py @@ -1333,6 +1333,42 @@ def test_transform_none_yields_dicts(lance_table): assert all("id" in item for item in items) +@pytest.mark.parametrize( + ("configured", "detected", "expected"), + [(2, 8, 2), (None, 3, 3), (None, None, 1)], +) +def test_transform_parallelism_configures_executor( + lance_table, monkeypatch, configured, detected, expected +): + """Explicit transform parallelism overrides the detected CPU count.""" + real_executor = streaming.ThreadPoolExecutor + monkeypatch.setattr(streaming.os, "cpu_count", lambda: detected) + + with patch.object(streaming, "ThreadPoolExecutor", wraps=real_executor) as executor: + list( + StreamingDataset( + lance_table, + num_splits=NUM_SPLITS, + shuffle_seed=SHUFFLE_SEED, + transform_parallelism=configured, + ) + ) + + assert executor.call_args_list[-1].kwargs["max_workers"] == expected + + +@pytest.mark.parametrize("transform_parallelism", [0, -1]) +def test_transform_parallelism_must_be_positive(lance_table, transform_parallelism): + with pytest.raises( + ValueError, match="transform_parallelism must be greater than 0" + ): + StreamingDataset( + lance_table, + num_splits=NUM_SPLITS, + transform_parallelism=transform_parallelism, + ) + + def test_filter_limits_rows(tmp_path): """A filter expression is applied to the permutation so only matching rows are yielded. IDs 0..59 pass ``id < 60``; the other 60 are excluded.""" From e5f489818b7b1cde8a3c871f76b28b18237364b9 Mon Sep 17 00:00:00 2001 From: Lance Release Date: Wed, 29 Jul 2026 07:11:47 +0000 Subject: [PATCH 12/29] =?UTF-8?q?Bump=20version:=200.37.0-beta.0=20?= =?UTF-8?q?=E2=86=92=200.37.1-beta.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.toml | 2 +- Cargo.lock | 6 +++--- docs/src/java/java.md | 2 +- java/lancedb-core/pom.xml | 2 +- java/pom.xml | 2 +- nodejs/Cargo.toml | 2 +- nodejs/npm/darwin-arm64/package.json | 2 +- nodejs/npm/linux-arm64-gnu/package.json | 2 +- nodejs/npm/linux-arm64-musl/package.json | 2 +- nodejs/npm/linux-x64-gnu/package.json | 2 +- nodejs/npm/linux-x64-musl/package.json | 2 +- nodejs/npm/win32-arm64-msvc/package.json | 2 +- nodejs/npm/win32-x64-msvc/package.json | 2 +- nodejs/package-lock.json | 4 ++-- nodejs/package.json | 2 +- python/Cargo.toml | 2 +- rust/lancedb/Cargo.toml | 2 +- 17 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index 57bcad5da..601b14d3f 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,5 +1,5 @@ [tool.bumpversion] -current_version = "0.37.0-beta.0" +current_version = "0.37.1-beta.0" parse = """(?x) (?P0|[1-9]\\d*)\\. (?P0|[1-9]\\d*)\\. diff --git a/Cargo.lock b/Cargo.lock index a105a0da3..0c4dabb20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5379,7 +5379,7 @@ dependencies = [ [[package]] name = "lancedb" -version = "0.37.0-beta.0" +version = "0.37.1-beta.0" dependencies = [ "ahash", "anyhow", @@ -5467,7 +5467,7 @@ dependencies = [ [[package]] name = "lancedb-nodejs" -version = "0.37.0-beta.0" +version = "0.37.1-beta.0" dependencies = [ "arrow-array", "arrow-buffer", @@ -5492,7 +5492,7 @@ dependencies = [ [[package]] name = "lancedb-python" -version = "0.37.0-beta.0" +version = "0.37.1-beta.0" dependencies = [ "arrow", "async-trait", diff --git a/docs/src/java/java.md b/docs/src/java/java.md index 891cc1674..11e901ad0 100644 --- a/docs/src/java/java.md +++ b/docs/src/java/java.md @@ -14,7 +14,7 @@ Add the following dependency to your `pom.xml`: com.lancedb lancedb-core - 0.37.0-beta.0 + 0.37.1-beta.0 ``` diff --git a/java/lancedb-core/pom.xml b/java/lancedb-core/pom.xml index eb47e25f3..3df2ac178 100644 --- a/java/lancedb-core/pom.xml +++ b/java/lancedb-core/pom.xml @@ -8,7 +8,7 @@ com.lancedb lancedb-parent - 0.37.0-beta.0 + 0.37.1-beta.0 ../pom.xml diff --git a/java/pom.xml b/java/pom.xml index ab8ec2cc8..ab62aa8ab 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -6,7 +6,7 @@ com.lancedb lancedb-parent - 0.37.0-beta.0 + 0.37.1-beta.0 pom ${project.artifactId} LanceDB Java SDK Parent POM diff --git a/nodejs/Cargo.toml b/nodejs/Cargo.toml index aef5fd54d..10e12edc8 100644 --- a/nodejs/Cargo.toml +++ b/nodejs/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lancedb-nodejs" edition.workspace = true -version = "0.37.0-beta.0" +version = "0.37.1-beta.0" publish = false license.workspace = true description.workspace = true diff --git a/nodejs/npm/darwin-arm64/package.json b/nodejs/npm/darwin-arm64/package.json index 618b02547..3c93ed470 100644 --- a/nodejs/npm/darwin-arm64/package.json +++ b/nodejs/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-darwin-arm64", - "version": "0.37.0-beta.0", + "version": "0.37.1-beta.0", "os": ["darwin"], "cpu": ["arm64"], "main": "lancedb.darwin-arm64.node", diff --git a/nodejs/npm/linux-arm64-gnu/package.json b/nodejs/npm/linux-arm64-gnu/package.json index 3eb4eeaba..5ade5aaa3 100644 --- a/nodejs/npm/linux-arm64-gnu/package.json +++ b/nodejs/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-linux-arm64-gnu", - "version": "0.37.0-beta.0", + "version": "0.37.1-beta.0", "os": ["linux"], "cpu": ["arm64"], "main": "lancedb.linux-arm64-gnu.node", diff --git a/nodejs/npm/linux-arm64-musl/package.json b/nodejs/npm/linux-arm64-musl/package.json index 7dbed5ae4..16bb0edd0 100644 --- a/nodejs/npm/linux-arm64-musl/package.json +++ b/nodejs/npm/linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-linux-arm64-musl", - "version": "0.37.0-beta.0", + "version": "0.37.1-beta.0", "os": ["linux"], "cpu": ["arm64"], "main": "lancedb.linux-arm64-musl.node", diff --git a/nodejs/npm/linux-x64-gnu/package.json b/nodejs/npm/linux-x64-gnu/package.json index 733aef750..6ee11e4bc 100644 --- a/nodejs/npm/linux-x64-gnu/package.json +++ b/nodejs/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-linux-x64-gnu", - "version": "0.37.0-beta.0", + "version": "0.37.1-beta.0", "os": ["linux"], "cpu": ["x64"], "main": "lancedb.linux-x64-gnu.node", diff --git a/nodejs/npm/linux-x64-musl/package.json b/nodejs/npm/linux-x64-musl/package.json index 041a49cb4..c2e15bb9f 100644 --- a/nodejs/npm/linux-x64-musl/package.json +++ b/nodejs/npm/linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-linux-x64-musl", - "version": "0.37.0-beta.0", + "version": "0.37.1-beta.0", "os": ["linux"], "cpu": ["x64"], "main": "lancedb.linux-x64-musl.node", diff --git a/nodejs/npm/win32-arm64-msvc/package.json b/nodejs/npm/win32-arm64-msvc/package.json index 2604d021e..d2820b1a1 100644 --- a/nodejs/npm/win32-arm64-msvc/package.json +++ b/nodejs/npm/win32-arm64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-win32-arm64-msvc", - "version": "0.37.0-beta.0", + "version": "0.37.1-beta.0", "os": [ "win32" ], diff --git a/nodejs/npm/win32-x64-msvc/package.json b/nodejs/npm/win32-x64-msvc/package.json index 2ee52df53..601b51380 100644 --- a/nodejs/npm/win32-x64-msvc/package.json +++ b/nodejs/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@lancedb/lancedb-win32-x64-msvc", - "version": "0.37.0-beta.0", + "version": "0.37.1-beta.0", "os": ["win32"], "cpu": ["x64"], "main": "lancedb.win32-x64-msvc.node", diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 2317c89a4..8e30b0fab 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lancedb/lancedb", - "version": "0.37.0-beta.0", + "version": "0.37.1-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lancedb/lancedb", - "version": "0.37.0-beta.0", + "version": "0.37.1-beta.0", "cpu": [ "x64", "arm64" diff --git a/nodejs/package.json b/nodejs/package.json index e9b861ad1..f3f719af2 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -11,7 +11,7 @@ "ann" ], "private": false, - "version": "0.37.0-beta.0", + "version": "0.37.1-beta.0", "main": "dist/index.js", "exports": { ".": "./dist/index.js", diff --git a/python/Cargo.toml b/python/Cargo.toml index a0e9394ec..cc706e712 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lancedb-python" -version = "0.37.0-beta.0" +version = "0.37.1-beta.0" publish = false edition.workspace = true description = "Python bindings for LanceDB" diff --git a/rust/lancedb/Cargo.toml b/rust/lancedb/Cargo.toml index ad6a4e2d6..66db3cf12 100644 --- a/rust/lancedb/Cargo.toml +++ b/rust/lancedb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lancedb" -version = "0.37.0-beta.0" +version = "0.37.1-beta.0" edition.workspace = true description = "LanceDB: A serverless, low-latency vector database for AI applications" license.workspace = true From f7feed48c31e1683a77f7ba9cd073975521d460b Mon Sep 17 00:00:00 2001 From: Yang Cen Date: Wed, 29 Jul 2026 17:40:12 +0800 Subject: [PATCH 13/29] feat(fts): support custom stop-word lists (#3734) ## What Expose custom FTS stop-word lists in the Python and TypeScript public APIs, including their standalone tokenize helpers and remote index creation. This PR supports concrete string lists only. It does not add file or LanceDB-table stop-word sources. ## Why Rust already exposes Lance's custom stop-word list option. The Python and TypeScript APIs did not pass it through, and local index details did not retain the full tokenizer parameters needed by index-backed tokenization after reopening a table. ## How - Add `custom_stop_words` / `customStopWords` to the Python and TypeScript FTS and tokenize options. - Preserve `None` / `undefined`, empty lists, and list contents without normalization. - Load the persisted FTS segment parameters when returning local index details. - Serialize the concrete list in remote create-index requests. - Keep Python and TypeScript tests thin; behavior, persistence, query tokenization, and remote JSON coverage live primarily in Rust. ## Validation - `cargo check --quiet --features remote --tests --examples` - `cargo clippy --quiet --features remote --tests --examples` - `cargo test --quiet --features remote --tests` - Python extension rebuild with `uv` and `maturin` - Targeted Python tests: 4 passed - Python `ruff format --check` and `ruff check` - TypeScript build, typecheck, Biome lint, generated docs, and targeted tests --------- Co-authored-by: Yang Cen --- docs/openapi.yml | 12 +++++++- docs/src/js/interfaces/FtsOptions.md | 15 ++++++++++ docs/src/js/interfaces/TokenizeOptions.md | 15 ++++++++++ docs/src/python/python.md | 30 +++++++++++++++++-- nodejs/__test__/remote.test.ts | 9 ++++-- nodejs/__test__/table.test.ts | 9 ++++++ nodejs/examples/full_text_search.test.ts | 8 ++++- nodejs/lancedb/index.ts | 11 +++++++ nodejs/lancedb/indices.ts | 11 +++++++ nodejs/src/index.rs | 4 +++ python/python/lancedb/__init__.py | 7 +++-- python/python/lancedb/_lancedb.pyi | 1 + python/python/lancedb/index.py | 8 ++++- python/python/lancedb/remote/table.py | 2 ++ python/python/lancedb/table.py | 7 +++++ python/python/tests/test_fts.py | 20 +++++++++++++ python/python/tests/test_remote_db.py | 2 ++ python/src/index.rs | 4 ++- python/src/table.rs | 5 +++- rust/lancedb/examples/full_text_search.rs | 7 ++++- rust/lancedb/src/remote/table.rs | 20 +++++++++---- rust/lancedb/src/table.rs | 33 ++++++++++++++++++--- rust/lancedb/src/table/create_index.rs | 36 ++++++++++++++++++++++- 23 files changed, 253 insertions(+), 23 deletions(-) diff --git a/docs/openapi.yml b/docs/openapi.yml index b07e47e04..2f9ae7d99 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -453,6 +453,16 @@ paths: The metric type to use for the index. l2, Cosine, Dot are supported. index_type: type: string + custom_stop_words: + type: [array, "null"] + items: + type: string + description: | + The custom stop-word list for an FTS index. A non-null + array replaces the language's built-in stop-word list and is only + applied when remove_stop_words is enabled. Null uses the built-in + language list, while an empty array explicitly replaces it with no + stop words. responses: "200": description: Index successfully created @@ -510,4 +520,4 @@ paths: "401": $ref: "#/components/responses/unauthorized" "404": - $ref: "#/components/responses/not_found" \ No newline at end of file + $ref: "#/components/responses/not_found" diff --git a/docs/src/js/interfaces/FtsOptions.md b/docs/src/js/interfaces/FtsOptions.md index b8b264fed..1d3c9ff82 100644 --- a/docs/src/js/interfaces/FtsOptions.md +++ b/docs/src/js/interfaces/FtsOptions.md @@ -56,6 +56,21 @@ the experimental FTS V3 format and may introduce breaking changes. *** +### customStopWords? + +```ts +optional customStopWords: string[]; +``` + +Custom stop words that replace the built-in list for `language`. + +This option only affects tokenization when `removeStopWords` is true. + +`undefined` keeps the built-in language list. An empty array explicitly +replaces it with no stop words. + +*** + ### language? ```ts diff --git a/docs/src/js/interfaces/TokenizeOptions.md b/docs/src/js/interfaces/TokenizeOptions.md index f061356d1..8ef15d03d 100644 --- a/docs/src/js/interfaces/TokenizeOptions.md +++ b/docs/src/js/interfaces/TokenizeOptions.md @@ -30,6 +30,21 @@ The tokenizer to use. The default is "simple". *** +### customStopWords? + +```ts +optional customStopWords: string[]; +``` + +Custom stop words that replace the built-in list for `language`. + +This option only affects tokenization when `removeStopWords` is true. + +`undefined` keeps the built-in language list. An empty array explicitly +replaces it with no stop words. + +*** + ### language? ```ts diff --git a/docs/src/python/python.md b/docs/src/python/python.md index 1e928be54..dd60451cc 100644 --- a/docs/src/python/python.md +++ b/docs/src/python/python.md @@ -94,9 +94,33 @@ of raw SQL strings with [where][lancedb.query.LanceQueryBuilder.where] and ## Full text search -Use [lancedb.table.Table.create_fts_index][] for the synchronous API or -[lancedb.table.AsyncTable.create_index][] with [lancedb.index.FTS][] for the -asynchronous API. +Pass `custom_stop_words` to [lancedb.index.FTS][]: + +```python +from lancedb.index import FTS + +table.create_index( + "text", + config=FTS(remove_stop_words=True, custom_stop_words=["acme", "internal"]), +) +``` + +The list replaces the built-in stop words and is used only when +`remove_stop_words=True`: + +- `custom_stop_words=None` uses the built-in list for `language`. +- `custom_stop_words=[]` removes no words. +- Values are passed through without trimming, lowercasing, or other rewriting. + +The same option is available on `lancedb.tokenize(...)` and the deprecated +[lancedb.table.Table.create_fts_index][] compatibility helper: + +```python +import lancedb + +tokens = list(lancedb.tokenize("acme makes searchable data", + custom_stop_words=["acme"])) +``` ::: lancedb.index.FTS diff --git a/nodejs/__test__/remote.test.ts b/nodejs/__test__/remote.test.ts index 22562b0bc..75c5540eb 100644 --- a/nodejs/__test__/remote.test.ts +++ b/nodejs/__test__/remote.test.ts @@ -226,7 +226,7 @@ describe("remote connection", () => { ); }); - it("sends the FTS posting block size to remote tables", async () => { + it("sends FTS options to remote tables", async () => { let createIndexBody: Record | undefined; await withMockDatabase( @@ -264,7 +264,11 @@ describe("remote connection", () => { async (db) => { const table = await db.openTable("t"); await table.createIndex("text", { - config: Index.fts({ blockSize: 256 }), + config: Index.fts({ + blockSize: 256, + removeStopWords: true, + customStopWords: ["the"], + }), }); }, ); @@ -272,6 +276,7 @@ describe("remote connection", () => { expect(createIndexBody?.["column"]).toBe("text"); expect(createIndexBody?.["index_type"]).toBe("FTS"); expect(createIndexBody?.["block_size"]).toBe(256); + expect(createIndexBody?.["custom_stop_words"]).toEqual(["the"]); }); it("diffs and merges remote branches", async () => { diff --git a/nodejs/__test__/table.test.ts b/nodejs/__test__/table.test.ts index 15248f5cf..41025047e 100644 --- a/nodejs/__test__/table.test.ts +++ b/nodejs/__test__/table.test.ts @@ -2769,6 +2769,15 @@ describe.each([arrow15, arrow16, arrow17, arrow18])( }, ); +test("tokenize supports custom stop words", async () => { + const tokens = await tokenize("the lance data", { + stem: false, + removeStopWords: true, + customStopWords: ["lance"], + }); + expect(tokens.map((token) => token.text)).toEqual(["the", "data"]); +}); + describe("when calling explainPlan", () => { let tmpDir: tmp.DirResult; let table: Table; diff --git a/nodejs/examples/full_text_search.test.ts b/nodejs/examples/full_text_search.test.ts index 9777bda7f..d1567dfbb 100644 --- a/nodejs/examples/full_text_search.test.ts +++ b/nodejs/examples/full_text_search.test.ts @@ -29,8 +29,14 @@ test("full text search", async () => { const tbl = await db.createTable("myVectors", data, { mode: "overwrite" }); await tbl.createIndex("doc", { - config: lancedb.Index.fts(), + config: lancedb.Index.fts({ + stem: false, + removeStopWords: true, + customStopWords: ["banana"], + }), }); + const tokens = await tbl.tokenize("apple banana", { column: "doc" }); + expect(tokens.map((token) => token.text)).toEqual(["apple"]); // --8<-- [start:full_text_search] const result = await tbl diff --git a/nodejs/lancedb/index.ts b/nodejs/lancedb/index.ts index cb91c30aa..63a4b08d0 100644 --- a/nodejs/lancedb/index.ts +++ b/nodejs/lancedb/index.ts @@ -194,6 +194,16 @@ export interface TokenizeOptions { /** Whether to remove stop words. */ removeStopWords?: boolean; + /** + * Custom stop words that replace the built-in list for `language`. + * + * This option only affects tokenization when `removeStopWords` is true. + * + * `undefined` keeps the built-in language list. An empty array explicitly + * replaces it with no stop words. + */ + customStopWords?: string[]; + /** Whether to fold ASCII characters. */ asciiFolding?: boolean; @@ -225,6 +235,7 @@ export async function tokenize( options?.lowercase, options?.stem, options?.removeStopWords, + options?.customStopWords, options?.asciiFolding, options?.ngramMinLength, options?.ngramMaxLength, diff --git a/nodejs/lancedb/indices.ts b/nodejs/lancedb/indices.ts index 9b438ef0f..dbeebf433 100644 --- a/nodejs/lancedb/indices.ts +++ b/nodejs/lancedb/indices.ts @@ -553,6 +553,16 @@ export interface FtsOptions { */ removeStopWords?: boolean; + /** + * Custom stop words that replace the built-in list for `language`. + * + * This option only affects tokenization when `removeStopWords` is true. + * + * `undefined` keeps the built-in language list. An empty array explicitly + * replaces it with no stop words. + */ + customStopWords?: string[]; + /** * whether to remove punctuation */ @@ -755,6 +765,7 @@ export class Index { options?.lowercase, options?.stem, options?.removeStopWords, + options?.customStopWords, options?.asciiFolding, options?.ngramMinLength, options?.ngramMaxLength, diff --git a/nodejs/src/index.rs b/nodejs/src/index.rs index 66dee7913..762f74870 100644 --- a/nodejs/src/index.rs +++ b/nodejs/src/index.rs @@ -43,6 +43,7 @@ pub fn tokenize( lower_case: Option, stem: Option, remove_stop_words: Option, + custom_stop_words: Option>, ascii_folding: Option, ngram_min_length: Option, ngram_max_length: Option, @@ -72,6 +73,7 @@ pub fn tokenize( if let Some(remove_stop_words) = remove_stop_words { opts = opts.remove_stop_words(remove_stop_words); } + opts = opts.custom_stop_words(custom_stop_words); if let Some(ascii_folding) = ascii_folding { opts = opts.ascii_folding(ascii_folding); } @@ -222,6 +224,7 @@ impl Index { lower_case: Option, stem: Option, remove_stop_words: Option, + custom_stop_words: Option>, ascii_folding: Option, ngram_min_length: Option, ngram_max_length: Option, @@ -250,6 +253,7 @@ impl Index { if let Some(remove_stop_words) = remove_stop_words { opts = opts.remove_stop_words(remove_stop_words); } + opts = opts.custom_stop_words(custom_stop_words); if let Some(ascii_folding) = ascii_folding { opts = opts.ascii_folding(ascii_folding); } diff --git a/python/python/lancedb/__init__.py b/python/python/lancedb/__init__.py index 6f6468d9c..71349b8a5 100644 --- a/python/python/lancedb/__init__.py +++ b/python/python/lancedb/__init__.py @@ -258,6 +258,7 @@ def tokenize( lower_case: bool = True, stem: bool = True, remove_stop_words: bool = True, + custom_stop_words: Optional[List[str]] = None, ascii_folding: bool = True, ngram_min_length: int = 3, ngram_max_length: int = 3, @@ -265,9 +266,10 @@ def tokenize( ) -> Iterable[FtsToken]: """Tokenize a full-text search query using an explicit tokenizer. - This does not require a table or FTS index. The tokenizer options match - :class:`lancedb.index.FTS`. + This does not require an FTS index. The tokenizer options match + :class:`lancedb.index.FTS`. ``custom_stop_words`` accepts a list of strings. """ + return _tokenize( query, base_tokenizer=base_tokenizer, @@ -276,6 +278,7 @@ def tokenize( lower_case=lower_case, stem=stem, remove_stop_words=remove_stop_words, + custom_stop_words=custom_stop_words, ascii_folding=ascii_folding, ngram_min_length=ngram_min_length, ngram_max_length=ngram_max_length, diff --git a/python/python/lancedb/_lancedb.pyi b/python/python/lancedb/_lancedb.pyi index 448947a29..67325e10b 100644 --- a/python/python/lancedb/_lancedb.pyi +++ b/python/python/lancedb/_lancedb.pyi @@ -59,6 +59,7 @@ def tokenize( lower_case: bool = True, stem: bool = True, remove_stop_words: bool = True, + custom_stop_words: Optional[List[str]] = None, ascii_folding: bool = True, ngram_min_length: int = 3, ngram_max_length: int = 3, diff --git a/python/python/lancedb/index.py b/python/python/lancedb/index.py index c06bb9903..5ced0600f 100644 --- a/python/python/lancedb/index.py +++ b/python/python/lancedb/index.py @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: Copyright The LanceDB Authors from dataclasses import dataclass -from typing import Literal, Optional +from typing import List, Literal, Optional from ._lancedb import ( IndexConfig, @@ -151,6 +151,11 @@ class FTS: remove_stop_words : bool, default True Whether to remove stop words. Stop words are common words that are often removed from text before indexing. For example, in English "the" and "and". + custom_stop_words : list of str, optional + Custom words replace the built-in language stop words + and only take effect when ``remove_stop_words`` is True. ``None`` uses + the built-in language list, while an empty list explicitly uses no + stop words. ascii_folding : bool, default True Whether to fold ASCII characters. This converts accented characters to their ASCII equivalent. For example, "café" would be converted to "cafe". @@ -179,6 +184,7 @@ class FTS: ngram_max_length: int = 3 prefix_only: bool = False block_size: int = 128 + custom_stop_words: Optional[List[str]] = None @dataclass diff --git a/python/python/lancedb/remote/table.py b/python/python/lancedb/remote/table.py index 682b8533c..01b90c019 100644 --- a/python/python/lancedb/remote/table.py +++ b/python/python/lancedb/remote/table.py @@ -340,6 +340,7 @@ class RemoteTable(Table): lower_case: bool = True, stem: bool = True, remove_stop_words: bool = True, + custom_stop_words: Optional[List[str]] = None, ascii_folding: bool = True, ngram_min_length: int = 3, ngram_max_length: int = 3, @@ -361,6 +362,7 @@ class RemoteTable(Table): lower_case=lower_case, stem=stem, remove_stop_words=remove_stop_words, + custom_stop_words=custom_stop_words, ascii_folding=ascii_folding, ngram_min_length=ngram_min_length, ngram_max_length=ngram_max_length, diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index 1da366a78..032c6dce7 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -1103,6 +1103,7 @@ class Table(ABC): lower_case: bool = True, stem: bool = True, remove_stop_words: bool = True, + custom_stop_words: Optional[List[str]] = None, ascii_folding: bool = True, ngram_min_length: int = 3, ngram_max_length: int = 3, @@ -1170,6 +1171,9 @@ class Table(ABC): remove_stop_words : bool, default True Whether to remove stop words. Stop words are common words that are often removed from text before indexing. For example, in English "the" and "and". + custom_stop_words : list of str, optional + Custom words that replace the built-in language stop words. ``None`` + uses the built-in list; an empty list explicitly uses no stop words. ascii_folding : bool, default True Whether to fold ASCII characters. This converts accented characters to their ASCII equivalent. For example, "café" would be converted to "cafe". @@ -3055,6 +3059,7 @@ class LanceTable(Table): lower_case: bool = True, stem: bool = True, remove_stop_words: bool = True, + custom_stop_words: Optional[List[str]] = None, ascii_folding: bool = True, ngram_min_length: int = 3, ngram_max_length: int = 3, @@ -3101,6 +3106,7 @@ class LanceTable(Table): "lower_case": lower_case, "stem": stem, "remove_stop_words": remove_stop_words, + "custom_stop_words": custom_stop_words, "ascii_folding": ascii_folding, "ngram_min_length": ngram_min_length, "ngram_max_length": ngram_max_length, @@ -3108,6 +3114,7 @@ class LanceTable(Table): } else: tokenizer_configs = self.infer_tokenizer_configs(tokenizer_name) + tokenizer_configs["custom_stop_words"] = custom_stop_words config = FTS(block_size=block_size, **tokenizer_configs) diff --git a/python/python/tests/test_fts.py b/python/python/tests/test_fts.py index 127510bc2..f791f9886 100644 --- a/python/python/tests/test_fts.py +++ b/python/python/tests/test_fts.py @@ -219,11 +219,13 @@ def test_create_inverted_index(table, with_position): table.create_fts_index( "text", with_position=with_position, + custom_stop_words=["puppy"], name="custom_fts_index", ) indices = table.list_indices() fts_indices = [i for i in indices if i.index_type == "FTS"] assert any(i.name == "custom_fts_index" for i in fts_indices) + assert fts_indices[0].index_details["custom_stop_words"] == ["puppy"] @pytest.mark.parametrize("block_size", [128, 256]) @@ -243,6 +245,24 @@ def test_create_inverted_index_rejects_invalid_block_size(table): table.create_index("text", config=FTS(block_size=129)) +def test_custom_stop_words_list(table): + table.create_index( + "text", + config=FTS(stem=False, custom_stop_words=["lance"]), + ) + + assert table.list_indices()[0].index_details["custom_stop_words"] == ["lance"] + tokens = table.tokenize("the lance data", column="text") + assert [token.text for token in tokens] == ["the", "data"] + empty_tokens = ldb.tokenize("the lance data", stem=False, custom_stop_words=[]) + assert [token.text for token in empty_tokens] == ["the", "lance", "data"] + with pytest.raises(TypeError, match=r"custom_stop_words.*int"): + ldb.tokenize( + "the lance data", + custom_stop_words=["lance", 42], + ) + + def test_search_fts(table): table.create_fts_index("text") results = table.search("puppy").select(["id", "text"]).limit(5).to_list() diff --git a/python/python/tests/test_remote_db.py b/python/python/tests/test_remote_db.py index 648a52b7a..55718f516 100644 --- a/python/python/tests/test_remote_db.py +++ b/python/python/tests/test_remote_db.py @@ -771,6 +771,7 @@ def test_table_create_indices(): "text", wait_timeout=timedelta(seconds=2), block_size=256, + custom_stop_words=["cloud"], name="custom_fts_idx", ) @@ -795,6 +796,7 @@ def test_table_create_indices(): assert "name" in fts_req assert fts_req["name"] == "custom_fts_idx" assert fts_req["block_size"] == 256 + assert fts_req["custom_stop_words"] == ["cloud"] # Check vector index request has custom name vector_req = received_requests[2] diff --git a/python/src/index.rs b/python/src/index.rs index a98b90cf9..8c81dcecf 100644 --- a/python/src/index.rs +++ b/python/src/index.rs @@ -59,7 +59,8 @@ pub fn extract_index_params(source: &Option>) -> PyResult>, ascii_folding: bool, ngram_min_length: u32, ngram_max_length: u32, diff --git a/python/src/table.rs b/python/src/table.rs index bb44fc023..84ba91f39 100644 --- a/python/src/table.rs +++ b/python/src/table.rs @@ -520,6 +520,7 @@ impl From for FtsToken { lower_case = true, stem = true, remove_stop_words = true, + custom_stop_words = None, ascii_folding = true, ngram_min_length = 3, ngram_max_length = 3, @@ -534,6 +535,7 @@ pub fn tokenize( lower_case: bool, stem: bool, remove_stop_words: bool, + custom_stop_words: Option>, ascii_folding: bool, ngram_min_length: u32, ngram_max_length: u32, @@ -555,7 +557,8 @@ pub fn tokenize( .ascii_folding(ascii_folding) .ngram_min_length(ngram_min_length) .ngram_max_length(ngram_max_length) - .ngram_prefix_only(prefix_only); + .ngram_prefix_only(prefix_only) + .custom_stop_words(custom_stop_words); let tokens = lancedb_tokenize(&query, ¶ms).infer_error()?; Ok(tokens.into_iter().map(FtsToken::from).collect()) } diff --git a/rust/lancedb/examples/full_text_search.rs b/rust/lancedb/examples/full_text_search.rs index 54d4a38f5..8d4b7557b 100644 --- a/rust/lancedb/examples/full_text_search.rs +++ b/rust/lancedb/examples/full_text_search.rs @@ -76,7 +76,12 @@ async fn create_table(db: &Connection) -> Result { async fn create_index(table: &Table) -> Result<()> { table - .create_index(&["doc"], Index::FTS(FtsIndexBuilder::default())) + .create_index( + &["doc"], + Index::FTS( + FtsIndexBuilder::default().custom_stop_words(Some(vec!["example".to_owned()])), + ), + ) .execute() .await?; Ok(()) diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index 4c3c1bc6f..685e2b54e 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -4553,6 +4553,19 @@ mod tests { }, Index::FTS(InvertedIndexParams::default().block_size(256).unwrap()), ), + ( + "FTS", + { + let mut body = serde_json::to_value(InvertedIndexParams::default()).unwrap(); + body["custom_stop_words"] = json!(["cat", " cat ", "CAT"]); + body + }, + Index::FTS(InvertedIndexParams::default().custom_stop_words(Some(vec![ + "cat".to_string(), + " cat ".to_string(), + "CAT".to_string(), + ]))), + ), ]; for (index_type, expected_body, index) in cases { @@ -5084,8 +5097,9 @@ mod tests { "max_token_length": 40, "lower_case": true, "stem": false, - "remove_stop_words": false, + "remove_stop_words": true, "ascii_folding": true, + "custom_stop_words": ["hello"], }) .to_string(); let table = Table::new_with_handler("my_table", move |request| { @@ -5123,10 +5137,6 @@ mod tests { assert_eq!( tokens, vec![ - FtsToken { - text: "hello".to_string(), - position: 0, - }, FtsToken { text: "こんにちは".to_string(), position: 1, diff --git a/rust/lancedb/src/table.rs b/rust/lancedb/src/table.rs index 0e9354ffc..00efd1f40 100644 --- a/rust/lancedb/src/table.rs +++ b/rust/lancedb/src/table.rs @@ -21,6 +21,7 @@ use lance::dataset::WriteMode; use lance::dataset::builder::DatasetBuilder; use lance::dataset::{InsertBuilder, WriteParams}; use lance::index::DatasetIndexExt; +use lance::index::scalar::load_segment_params; use lance::io::{ObjectStoreParams, WrappingObjectStore}; use lance_datafusion::utils::StreamingWriteSource; use lance_index::IndexCriteria; @@ -3193,10 +3194,9 @@ impl BaseTable for NativeTable { async fn list_indices(&self) -> Result> { let dataset = self.dataset.get().await?; let total_rows = dataset.count_rows(None).await? as u64; - let indices = dataset - .describe_indices(None) - .await? - .into_iter() + let descriptions = dataset.describe_indices(None).await?; + let mut indices: Vec = descriptions + .iter() .filter_map(|idx_desc| { let index_type: crate::index::IndexType = idx_desc .index_type() @@ -3254,6 +3254,31 @@ impl BaseTable for NativeTable { }) }) .collect(); + + for index in indices + .iter_mut() + .filter(|index| index.index_type == crate::index::IndexType::FTS) + { + let Some(description) = descriptions + .iter() + .find(|description| description.name() == index.name) + else { + continue; + }; + let segments = description.segments(); + let Some(segment) = segments.first() else { + continue; + }; + let params = load_segment_params(&dataset, segment).await?; + let details = serde_json::to_string(¶ms).map_err(|source| Error::Other { + message: format!( + "Failed to serialize full text search configuration for index '{}'", + index.name + ), + source: Some(Box::new(source)), + })?; + index.index_details = Some(details); + } Ok(indices) } diff --git a/rust/lancedb/src/table/create_index.rs b/rust/lancedb/src/table/create_index.rs index 6be9de0e6..ab412a502 100644 --- a/rust/lancedb/src/table/create_index.rs +++ b/rust/lancedb/src/table/create_index.rs @@ -1366,11 +1366,19 @@ mod tests { table .create_index( &["text"], - Index::FTS(FtsIndexBuilder::default().block_size(256).unwrap()), + Index::FTS( + FtsIndexBuilder::default() + .stem(false) + .custom_stop_words(Some(vec!["cat".to_string()])) + .block_size(256) + .unwrap(), + ), ) .execute() .await .unwrap(); + drop(table); + let table = conn.open_table("test_bitmap").execute().await.unwrap(); let index_configs = table.list_indices().await.unwrap(); assert_eq!(index_configs.len(), 1); let index = index_configs.into_iter().next().unwrap(); @@ -1381,6 +1389,32 @@ mod tests { let index_params: FtsIndexBuilder = serde_json::from_str(index.index_details.as_deref().unwrap()).unwrap(); assert_eq!(index_params.posting_block_size(), 256); + assert_eq!( + serde_json::to_value(&index_params).unwrap()["custom_stop_words"], + serde_json::json!(["cat"]) + ); + assert_eq!( + table + .tokenize("cat dog", "text_idx") + .await + .unwrap() + .into_iter() + .map(|token| token.text) + .collect::>(), + vec!["dog"] + ); + + let batches = table + .query() + .full_text_search(FullTextSearchQuery::new("cat dog".to_string())) + .limit(120) + .execute() + .await + .unwrap() + .try_collect::>() + .await + .unwrap(); + assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::(), 40); let num_rows = 120; let stats = table.index_stats("text_idx").await.unwrap().unwrap(); From 03b26d585ba89b764679ad34bdbf506adffbe309 Mon Sep 17 00:00:00 2001 From: Will Jones Date: Wed, 29 Jul 2026 13:06:41 -0700 Subject: [PATCH 14/29] fix: deflake test_read_consistency_interval (#3713) `test_read_consistency_interval` asserted that a table opened with a 100ms `read_consistency_interval` still read stale data immediately after a concurrent write. The cache timestamp is set when the table is opened and reads within the interval do not refresh it, so that assertion only held if the intervening open/count/commit/count sequence finished within 100ms of real wall-clock time. On a loaded CI runner it did not: the TTL expired, `count_rows` refreshed synchronously, and the test failed with `left: 1, right: 0`. This broke the Rust workflow on `main` at 0bc08160 (a Python-only commit). This pins the `background_cache` mock clock once `table2` has seeded its cache, and advances it explicitly in place of `tokio::time::sleep`, so the test controls when the interval elapses. Same approach as #3547. With the clock pinned there is no real sleep left to be imprecise, so the `cfg(not(target_os = "windows"))` guard is dropped and the test now runs on Windows too. Verified by inserting a stall before the write: 120ms reproduces the original failure deterministically, and with this change the test still passes with a 500ms stall. Fixes #3712 --- rust/lancedb/src/table.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/rust/lancedb/src/table.rs b/rust/lancedb/src/table.rs index 00efd1f40..7b654e194 100644 --- a/rust/lancedb/src/table.rs +++ b/rust/lancedb/src/table.rs @@ -4136,10 +4136,10 @@ mod tests { Box::new(RecordBatchIterator::new(vec![Ok(batch)], schema)) } - // Windows does not support precise sleep durations due to timer resolution limitations. - #[cfg(not(target_os = "windows"))] #[tokio::test] async fn test_read_consistency_interval() { + use crate::utils::background_cache::clock; + let intervals = vec![ None, Some(0), @@ -4166,6 +4166,12 @@ mod tests { let conn2 = conn2.execute().await.unwrap(); let table2 = conn2.open_table("my_table").execute().await.unwrap(); + // Freeze the consistency clock now that `table2` has seeded its cache, so the + // interval only elapses when this test advances it. Otherwise the write and + // count_rows calls below race the real 100ms interval, which a loaded CI + // runner loses. Must come after open_table: creating the cache clears the mock. + clock::pin(); + assert_eq!(table1.count_rows(None).await.unwrap(), 0); assert_eq!(table2.count_rows(None).await.unwrap(), 0); @@ -4183,7 +4189,7 @@ mod tests { } Some(100) => { assert_eq!(table2.count_rows(None).await.unwrap(), 0); - tokio::time::sleep(Duration::from_millis(100)).await; + clock::advance_by(Duration::from_millis(100)); assert_eq!(table2.count_rows(None).await.unwrap(), 1); } _ => unreachable!(), From 1ad6ce3a4eaf3c475ca14d4d87cf579a295f5eca Mon Sep 17 00:00:00 2001 From: LanceDB Robot Date: Wed, 29 Jul 2026 18:14:27 -0700 Subject: [PATCH 15/29] chore: update lance dependency to v10.0.0-beta.7 (#3745) Updates the Rust workspace Lance dependencies and Java lance-core dependency to v10.0.0-beta.7. No compatibility fixes were required; full workspace clippy passed with warnings denied. Lance tag: https://github.com/lance-format/lance/releases/tag/v10.0.0-beta.7 --------- Co-authored-by: Lu Qiu Co-authored-by: Claude Fable 5 --- Cargo.lock | 85 +++++++++++++------------- Cargo.toml | 28 ++++----- java/pom.xml | 2 +- python/python/tests/test_blob.py | 6 +- rust/lancedb/src/error.rs | 3 + rust/lancedb/src/table.rs | 2 +- rust/lancedb/tests/blob_integration.rs | 10 +-- 7 files changed, 70 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c4dabb20..76a15794b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3421,8 +3421,8 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsst" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow-array", "rand 0.9.5", @@ -4777,8 +4777,8 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" [[package]] name = "lance" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arc-swap", "arrow", @@ -4852,8 +4852,8 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow-array", "arrow-buffer", @@ -4875,7 +4875,7 @@ dependencies = [ [[package]] name = "lance-arrow-scalar" version = "58.0.0" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow-array", "arrow-buffer", @@ -4889,7 +4889,7 @@ dependencies = [ [[package]] name = "lance-arrow-stats" version = "58.0.0" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow-array", "arrow-schema", @@ -4898,8 +4898,8 @@ dependencies = [ [[package]] name = "lance-bitpacking" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrayref", "crunchy", @@ -4909,14 +4909,15 @@ dependencies = [ [[package]] name = "lance-core" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow-array", "arrow-buffer", "arrow-data", "arrow-schema", "async-trait", + "blake3", "byteorder", "bytes", "datafusion-common", @@ -4949,8 +4950,8 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow", "arrow-array", @@ -4980,8 +4981,8 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow", "arrow-array", @@ -4998,8 +4999,8 @@ dependencies = [ [[package]] name = "lance-derive" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "proc-macro2", "quote", @@ -5008,8 +5009,8 @@ dependencies = [ [[package]] name = "lance-encoding" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow-arith", "arrow-array", @@ -5044,8 +5045,8 @@ dependencies = [ [[package]] name = "lance-file" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow-arith", "arrow-array", @@ -5075,8 +5076,8 @@ dependencies = [ [[package]] name = "lance-index" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arc-swap", "arrow", @@ -5143,8 +5144,8 @@ dependencies = [ [[package]] name = "lance-index-core" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow-array", "arrow-schema", @@ -5166,8 +5167,8 @@ dependencies = [ [[package]] name = "lance-io" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow", "arrow-arith", @@ -5210,8 +5211,8 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow-array", "arrow-buffer", @@ -5227,8 +5228,8 @@ dependencies = [ [[package]] name = "lance-namespace" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow", "async-trait", @@ -5240,8 +5241,8 @@ dependencies = [ [[package]] name = "lance-namespace-impls" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow", "arrow-ipc", @@ -5295,8 +5296,8 @@ dependencies = [ [[package]] name = "lance-select" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow-array", "arrow-buffer", @@ -5311,8 +5312,8 @@ dependencies = [ [[package]] name = "lance-table" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow", "arrow-array", @@ -5351,8 +5352,8 @@ dependencies = [ [[package]] name = "lance-testing" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "arrow-array", "arrow-schema", @@ -5365,8 +5366,8 @@ dependencies = [ [[package]] name = "lance-tokenizer" -version = "10.0.0-beta.5" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.5#ddb8e28ca238f29628b8e1795ddccbb7bf75e5c8" +version = "10.0.0-beta.7" +source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" dependencies = [ "icu_segmenter", "jieba-rs", diff --git a/Cargo.toml b/Cargo.toml index fee01b732..9638da17f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,20 +13,20 @@ categories = ["database-implementations"] rust-version = "1.91.0" [workspace.dependencies] -lance = { "version" = "=10.0.0-beta.5", default-features = false, "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-core = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-datagen = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-file = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-io = { "version" = "=10.0.0-beta.5", default-features = false, "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-index = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-linalg = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-namespace = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-namespace-impls = { "version" = "=10.0.0-beta.5", default-features = false, "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-table = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-testing = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-datafusion = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-encoding = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } -lance-arrow = { "version" = "=10.0.0-beta.5", "tag" = "v10.0.0-beta.5", "git" = "https://github.com/lance-format/lance.git" } +lance = { "version" = "=10.0.0-beta.7", default-features = false, "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-core = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-datagen = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-file = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-io = { "version" = "=10.0.0-beta.7", default-features = false, "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-index = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-linalg = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-namespace = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-namespace-impls = { "version" = "=10.0.0-beta.7", default-features = false, "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-table = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-testing = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-datafusion = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-encoding = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance-arrow = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } ahash = "0.8" # Note that this one does not include pyarrow arrow = { version = "58.0.0", optional = false } diff --git a/java/pom.xml b/java/pom.xml index ab62aa8ab..7e5194159 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -28,7 +28,7 @@ UTF-8 15.0.0 - 10.0.0-beta.5 + 10.0.0-beta.7 false 2.30.0 1.7 diff --git a/python/python/tests/test_blob.py b/python/python/tests/test_blob.py index 04a99ea16..b205c20a6 100644 --- a/python/python/tests/test_blob.py +++ b/python/python/tests/test_blob.py @@ -226,13 +226,13 @@ def test_fetch_blob_ranges_validates_requests(): table = _blob_table("range_validation", [{"id": 1, "image": b"abc"}]) row_id = _row_ids_by_id(table)[1] - with pytest.raises(RuntimeError, match="exceeds blob size"): + with pytest.raises(ValueError, match="exceeds blob size"): table.fetch_blob_ranges("image", [(row_id, 2, 2)]) - with pytest.raises(RuntimeError, match="offset \\+ length overflowed"): + with pytest.raises(ValueError, match="offset \\+ length overflowed"): table.fetch_blob_ranges("image", [(row_id, 2**64 - 1, 1)]) - with pytest.raises(ValueError, match="row ids"): + with pytest.raises(ValueError, match="row IDs"): table.fetch_blob_ranges("image", [(2**64 - 1, 0, 1)]) diff --git a/rust/lancedb/src/error.rs b/rust/lancedb/src/error.rs index 7bfbc6f2e..37dfc44fc 100644 --- a/rust/lancedb/src/error.rs +++ b/rust/lancedb/src/error.rs @@ -121,6 +121,9 @@ impl From for Error { match source { lance::Error::Wrapped { error, .. } => Self::from_box_error(error), lance::Error::External { source } => Self::from_box_error(source), + lance::Error::InvalidInput { source, .. } => Self::InvalidInput { + message: source.to_string(), + }, _ => Self::Lance { source }, } } diff --git a/rust/lancedb/src/table.rs b/rust/lancedb/src/table.rs index 7b654e194..e0ab9754c 100644 --- a/rust/lancedb/src/table.rs +++ b/rust/lancedb/src/table.rs @@ -4591,7 +4591,7 @@ mod tests { .set_lsm_write_spec(LsmWriteSpec::bucket("id", bad)) .await .expect_err("should reject"); - assert!(matches!(err, Error::Lance { .. }), "got {:?}", err); + assert!(matches!(err, Error::InvalidInput { .. }), "got {:?}", err); } // Happy path: install spec; verify MemWAL details record it. diff --git a/rust/lancedb/tests/blob_integration.rs b/rust/lancedb/tests/blob_integration.rs index 47b80b433..e1fe742f7 100644 --- a/rust/lancedb/tests/blob_integration.rs +++ b/rust/lancedb/tests/blob_integration.rs @@ -647,7 +647,7 @@ async fn fetch_blob_ranges_validates_requests() -> Result<()> { .await .unwrap_err(); assert!(matches!(&err, Error::InvalidInput { .. }), "got {err:?}"); - assert!(err.to_string().contains("row ids")); + assert!(err.to_string().contains("row IDs")); Ok(()) } @@ -680,7 +680,7 @@ async fn fetch_blobs_out_of_range_id_errors_without_panic() -> Result<()> { let table = create_inline_blob_table(&db, "t", &[1], &[Some(b"x".as_slice())]).await?; let err = table.fetch_blobs("image", &[u64::MAX]).await.unwrap_err(); - assert!(err.to_string().contains("row ids")); + assert!(err.to_string().contains("row IDs")); Ok(()) } @@ -694,11 +694,11 @@ async fn fetch_blob_apis_reject_mixed_valid_and_missing_row_ids() -> Result<()> let err = table.fetch_blobs("image", &row_ids).await.unwrap_err(); assert!(matches!(&err, Error::InvalidInput { .. }), "got {err:?}"); - assert!(err.to_string().contains("row ids")); + assert!(err.to_string().contains("row IDs")); let err = table.fetch_blob_files("image", &row_ids).await.unwrap_err(); assert!(matches!(&err, Error::InvalidInput { .. }), "got {err:?}"); - assert!(err.to_string().contains("row ids")); + assert!(err.to_string().contains("row IDs")); let requests = row_ids.map(|row_id| BlobRangeRequest::new(row_id, 0, 1)); let err = table @@ -706,7 +706,7 @@ async fn fetch_blob_apis_reject_mixed_valid_and_missing_row_ids() -> Result<()> .await .unwrap_err(); assert!(matches!(&err, Error::InvalidInput { .. }), "got {err:?}"); - assert!(err.to_string().contains("row ids")); + assert!(err.to_string().contains("row IDs")); Ok(()) } From 4dc2d9a0f29602ad15094cbd5da97de4ede90bca Mon Sep 17 00:00:00 2001 From: heart Date: Thu, 30 Jul 2026 22:56:13 +0800 Subject: [PATCH 16/29] fix(python): avoid async work in sync reprs (#3620) ## Summary - keep the existing synchronous `connect()` path unchanged - make `LanceDBConnection.__repr__` and `LanceTable.__repr__` side-effect-free - add a regression test that verifies sync reprs do not call the Python background loop ## Root cause The freeze is caused by debugger rendering, not by `connect()` itself: 1. debugpy stops at a breakpoint and suspends all Python threads. 2. The debugger renders the new `db_connection` local by calling `repr()`. 3. `LanceDBConnection.__repr__` reads `read_consistency_interval`. 4. That property calls `LOOP.run(...).result()`. 5. The `LanceDBBackgroundEventLoop` thread is suspended by the debugger, so `repr()` waits for a thread that cannot run. This explains why the symptom appears immediately after `connect()`: it is the first point where a connection object exists in locals and is automatically rendered. `LanceTable.__repr__` had the same problem because it also read the connection's consistency interval. This follows the same principle as #3411: `__repr__` must not trigger async work or I/O that a debugger assumes is lightweight. ## Evidence I reproduced the behavior with the real LanceDB classes and debugpy 1.8.21 using a DAP client: - latest `main` (`ff6ff099`): the debugger reported `allThreadsStopped: true`, and evaluating `repr(db_connection)` timed out - this branch (`5755a5ba`): the same evaluation returned `LanceDBConnection(uri='/tmp/lancedb-debug-repro')` immediately - setting `PYDEVD_UNBLOCK_THREADS_TIMEOUT=0` also allowed the original repr path to complete, independently confirming that it was waiting on a suspended thread The regression test creates a connection and table, replaces `LOOP.run` with a function that fails, and verifies that both reprs still work. ## Validation - `maturin develop --manifest-path python/Cargo.toml` - `python -m pytest python/python/tests/test_db.py::test_sync_repr_does_not_use_background_loop python/python/tests/test_table.py::test_consistency -q` (`4 passed`) - `ruff check .` - `ruff format --check python/python/lancedb/db.py python/python/lancedb/table.py python/python/tests/test_db.py python/python/tests/test_table.py` - `git diff --check` Refs #3611. --- python/python/lancedb/db.py | 6 +----- python/python/lancedb/table.py | 8 +------- python/python/tests/test_db.py | 15 +++++++++++++++ python/python/tests/test_table.py | 3 --- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/python/python/lancedb/db.py b/python/python/lancedb/db.py index 0db9003d1..fd6240d48 100644 --- a/python/python/lancedb/db.py +++ b/python/python/lancedb/db.py @@ -688,11 +688,7 @@ class LanceDBConnection(DBConnection): return cls(None, _inner=inner) def __repr__(self) -> str: - val = f"{self.__class__.__name__}(uri={self._conn.uri!r}" - if self.read_consistency_interval is not None: - val += f", read_consistency_interval={repr(self.read_consistency_interval)}" - val += ")" - return val + return f"{self.__class__.__name__}(uri={self._conn.uri!r})" @override def serialize(self) -> str: diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index 032c6dce7..56764bfd3 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -2468,13 +2468,7 @@ class LanceTable(Table): return LOOP.run(self._table.count_rows(filter)) def __repr__(self) -> str: - val = f"{self.__class__.__name__}(name={self.name!r}" - if self._conn.read_consistency_interval is not None: - val += ", read_consistency_interval={!r}".format( - self._conn.read_consistency_interval - ) - val += f", _conn={self._conn!r})" - return val + return f"{self.__class__.__name__}(name={self.name!r}, _conn={self._conn!r})" def __str__(self) -> str: return self.__repr__() diff --git a/python/python/tests/test_db.py b/python/python/tests/test_db.py index 2bc4a0611..86b70b75d 100644 --- a/python/python/tests/test_db.py +++ b/python/python/tests/test_db.py @@ -62,6 +62,21 @@ def test_basic(tmp_path): assert db.open_table("test").name == db["test"].name +def test_sync_repr_does_not_use_background_loop(tmp_path, monkeypatch): + from lancedb.background_loop import LOOP + + db = lancedb.connect(tmp_path) + table = db.create_table("test", data=[{"id": 1}]) + + def fail_run(*args, **kwargs): + raise AssertionError("repr should not use the Python background loop") + + monkeypatch.setattr(LOOP, "run", fail_run) + + assert repr(db) == f"LanceDBConnection(uri={str(tmp_path)!r})" + assert repr(table) == f"LanceTable(name='test', _conn={db!r})" + + def test_ingest_pd(tmp_path): db = lancedb.connect(tmp_path) diff --git a/python/python/tests/test_table.py b/python/python/tests/test_table.py index b281e74da..2a34084cf 100644 --- a/python/python/tests/test_table.py +++ b/python/python/tests/test_table.py @@ -3087,9 +3087,6 @@ def test_consistency(tmp_path, consistency_interval): db2 = lancedb.connect(tmp_path, read_consistency_interval=consistency_interval) table2 = db2.open_table("my_table") - if consistency_interval is not None: - assert "read_consistency_interval=datetime.timedelta(" in repr(db2) - assert "read_consistency_interval=datetime.timedelta(" in repr(table2) assert table2.version == table.version table.add([{"id": 1}]) From 7dfdfe64019485f7ee625394d007b160fcb9cb42 Mon Sep 17 00:00:00 2001 From: sanskar singh bhardwaj <95009647+sanskar-singh-2403@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:26:25 +0530 Subject: [PATCH 17/29] fix(remote): surface masked merge_insert stream errors under HTTP2 (#2339) (#3711) ## Summary Fixes #2339. `merge_insert()` on the remote client could mask the real cause of a mid-stream input error, reporting only: > stream error sent by user: unexpected internal error ## Root cause There were two divergent streaming-write code paths in the remote client: - `add()` uses `RemoteInsertExec`, which streams the request body through a `tokio::sync::oneshot` error side-channel and drains it before reporting the HTTP result. If the input stream errors mid-body, the original error is recovered. - `merge_insert()` used a legacy path (`send_streaming` -> `reader_as_body`) that piped arrow `Some(Err(e))` straight into the HTTP2 request body. Hyper swallows body-stream errors under HTTP2 (see hyperium/hyper#2547), so the original error was lost and only the generic transport error surfaced. ## Fix Consolidate both write paths onto the side-channel mechanism instead of patching the legacy path: - Generalize `RemoteInsertExec` into `RemoteWriteExec`, carrying a `WriteOp` enum (`Insert { overwrite }` | `MergeInsert { query, timeout }`) that selects the endpoint, query params, request-timeout header, and response parsing. The executor returns a `WriteResult` enum (`Add` | `Merge`) with typed accessors, and `with_new_children` still resets the result so the rescannable retry loop is unaffected. - Route `merge_insert()` through `RemoteWriteExec`. The public API only accepts a `RecordBatchReader` (not rescannable), so the reader is buffered into a `Vec` before the retry loop to preserve the previous retry-on-retryable-status behaviour. This mirrors what the old `send_streaming(with_retry=true)` path already did. - Remove the now-unused `send_streaming` / `reader_as_body` / `buffer_reader` / `make_reader` helpers. Multipart stays insert-only (the server has no multipart merge_insert endpoint), so that hot path is behaviorally unchanged. ## Testing - Added `test_merge_insert_input_error_surfaces_original`, which drives an erroring input through the single-request `merge_insert` path and asserts the original error (`boom`) is surfaced rather than the masked HTTP error. Confirmed it fails without the side-channel drain (it then reports a masked `500 ... request or response body error`). - Full suite green: `cargo test -p lancedb --lib --features remote` -> 694 passed, 0 failed. Includes the existing `test_merge_insert_retries_on_409`, confirming retry behaviour is preserved. --- rust/lancedb/src/remote/table.rs | 194 ++++++--------- rust/lancedb/src/remote/table/insert.rs | 307 ++++++++++++++++++------ 2 files changed, 310 insertions(+), 191 deletions(-) diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index 685e2b54e..b0d7dacc5 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -3,10 +3,13 @@ pub mod insert; -use self::insert::RemoteInsertExec; +use self::insert::{RemoteWriteExec, WriteOp}; use crate::expr::expr_to_sql_string; use crate::table::write_progress::FinishOnDrop; - +// Used by the test module below (single-request write requests set these +// headers directly in test handlers); kept at module scope so both the +// library and its tests can name them. +#[cfg(test)] use super::ARROW_STREAM_CONTENT_TYPE; use super::client::RequestResultExt; use super::client::{HttpSend, RestfulLanceDbClient, Sender}; @@ -44,7 +47,7 @@ use crate::{ merge::MergeInsertBuilder, }, }; -use arrow_array::{RecordBatch, RecordBatchIterator, RecordBatchReader}; +use arrow_array::RecordBatchReader; use arrow_ipc::reader::FileReader; use arrow_schema::{DataType, SchemaRef}; use async_trait::async_trait; @@ -53,6 +56,7 @@ use datafusion_common::DataFusionError; use datafusion_physical_plan::stream::RecordBatchStreamAdapter; use datafusion_physical_plan::{ExecutionPlan, RecordBatchStream, SendableRecordBatchStream}; use futures::{StreamExt, TryStreamExt}; +#[cfg(test)] use http::header::CONTENT_TYPE; use http::{HeaderName, StatusCode}; use lance::arrow::json::{JsonDataType, JsonSchema}; @@ -455,50 +459,6 @@ impl RemoteTable { } } - fn reader_as_body(data: Box) -> Result { - // TODO: Once Phalanx supports compression, we should use it here. - let mut writer = arrow_ipc::writer::StreamWriter::try_new( - Vec::new(), - &RecordBatchReader::schema(&*data), - )?; - - // Mutex is just here to make it sync. We shouldn't have any contention. - let mut data = Mutex::new(data); - let body_iter = std::iter::from_fn(move || match data.get_mut().unwrap().next() { - Some(Ok(batch)) => { - writer.write(&batch).ok()?; - let buffer = std::mem::take(writer.get_mut()); - Some(Ok(buffer)) - } - Some(Err(e)) => Some(Err(e)), - None => { - writer.finish().ok()?; - let buffer = std::mem::take(writer.get_mut()); - Some(Ok(buffer)) - } - }); - let body_stream = futures::stream::iter(body_iter); - Ok(reqwest::Body::wrap_stream(body_stream)) - } - - /// Buffer the reader into memory - async fn buffer_reader( - reader: &mut R, - ) -> Result<(SchemaRef, Vec)> { - let schema = reader.schema(); - let mut batches = Vec::new(); - for batch in reader { - batches.push(batch?); - } - Ok((schema, batches)) - } - - /// Create a new RecordBatchReader from buffered data - fn make_reader(schema: SchemaRef, batches: Vec) -> impl RecordBatchReader { - let iter = batches.into_iter().map(Ok); - RecordBatchIterator::new(iter, schema) - } - async fn send(&self, req: RequestBuilder, with_retry: bool) -> Result<(String, Response)> { let res = if with_retry { self.client.send_with_retry(req, None, true).await? @@ -508,34 +468,6 @@ impl RemoteTable { Ok(res) } - /// Send the request with streaming body. - /// This will use retries if with_retry is set and the number of configured retries is > 0. - /// If retries are enabled, the stream will be buffered into memory. - async fn send_streaming( - &self, - req: RequestBuilder, - mut data: Box, - with_retry: bool, - ) -> Result<(String, Response)> { - if !with_retry || self.client.retry_config.retries == 0 { - let body = Self::reader_as_body(data)?; - return self.client.send(req.body(body)).await; - } - - // to support retries, buffer into memory and clone the batches on each retry - let (schema, batches) = Self::buffer_reader(&mut *data).await?; - let make_body = Box::new(move || { - let reader = Self::make_reader(schema.clone(), batches.clone()); - Self::reader_as_body(Box::new(reader)) - }); - let res = self - .client - .send_with_retry(req, Some(make_body), false) - .await?; - - Ok(res) - } - pub(super) async fn handle_table_not_found( table_name: &str, response: reqwest::Response, @@ -1238,12 +1170,14 @@ impl RemoteTable { let _guard = output.tracker.as_ref().map(|t| t.track_task()); - let mut insert: Arc = Arc::new(RemoteInsertExec::new( + let mut insert: Arc = Arc::new(RemoteWriteExec::new( self.name.clone(), self.identifier.clone(), self.client.clone(), output.plan, - output.overwrite, + WriteOp::Insert { + overwrite: output.overwrite, + }, output.tracker.clone(), self.branch.clone(), )); @@ -1258,7 +1192,7 @@ impl RemoteTable { match result { Ok(_) => { let add_result = (insert.as_ref() as &dyn std::any::Any) - .downcast_ref::>() + .downcast_ref::>() .and_then(|i| i.add_result()) .unwrap_or(AddResult { version: 0 }); @@ -1359,7 +1293,7 @@ impl RemoteTable { )?, ) as Arc; - let insert = Arc::new(RemoteInsertExec::new_multipart( + let insert = Arc::new(RemoteWriteExec::new_multipart( self.name.clone(), self.identifier.clone(), self.client.clone(), @@ -2354,48 +2288,66 @@ impl BaseTable for RemoteTable { self.check_mutable().await?; let timeout = params.timeout; - let query = MergeInsertRequest::try_from(params)?; - let mut request = self - .client - .post(&format!("/v1/table/{}/merge_insert/", self.identifier)) - .query(&query) - .header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE); - request = self.apply_branch_query(request); - if let Some(timeout) = timeout { - // (If it doesn't fit into u64, it's not worth sending anyways.) - if let Ok(timeout_ms) = u64::try_from(timeout.as_millis()) { - request = request.header(REQUEST_TIMEOUT_HEADER, timeout_ms); + // Drive merge_insert through the same RemoteWriteExec streaming path as + // add(). This routes the request body through the error side-channel so + // an input stream error (e.g. NaN rejection) surfaces with its original + // message instead of the masked HTTP error Hyper produces when a request + // body stream fails under HTTP2 (issue #2339). The branch, request + // timeout header, and merge query params are all applied inside the exec. + // + // The public merge_insert API only accepts a `RecordBatchReader`, which + // is not rescannable and so could not be retried directly. To preserve + // the previous retry-on-retryable-status behaviour, buffer the reader + // into memory first: a `Vec` is rescannable, so the outer + // loop can re-execute the plan (and re-stream the body) on each retry. + // This mirrors the old `send_streaming(with_retry=true)` path, which + // likewise buffered the reader to support retries. + let batches = new_data.collect::, _>>()?; + let source: Box = Box::new(batches); + let rescannable = source.rescannable(); + let input: Arc = + Arc::new(crate::table::datafusion::scannable_exec::ScannableExec::new(source, None)); + + let mut merge: Arc = Arc::new(RemoteWriteExec::new( + self.name.clone(), + self.identifier.clone(), + self.client.clone(), + input, + WriteOp::MergeInsert { query, timeout }, + None, + self.branch.clone(), + )); + + let mut retry_counter = crate::remote::retry::RetryCounter::new( + &self.client.retry_config, + uuid::Uuid::new_v4().to_string(), + ); + + loop { + let stream = execute_plan(merge.clone(), Default::default())?; + let result: Result> = stream.try_collect().await.map_err(Error::from); + + match result { + Ok(_) => { + let merge_result = (merge.as_ref() as &dyn std::any::Any) + .downcast_ref::>() + .and_then(|m| m.merge_result()) + .unwrap_or_default(); + + self.track_write_version(merge_result.version); + return Ok(merge_result); + } + Err(err) if rescannable && self.is_retryable_write_error(&err) => { + retry_counter.increment_from_error(err)?; + tokio::time::sleep(retry_counter.next_sleep_time()).await; + merge = merge.reset_state()?; + continue; + } + Err(err) => return Err(err), } } - - let (request_id, response) = self.send_streaming(request, new_data, true).await?; - - let response = self.check_table_response(&request_id, response).await?; - let body = response.text().await.err_to_http(request_id.clone())?; - - if body.trim().is_empty() { - // Backward compatible with old servers - return Ok(MergeResult { - version: 0, - num_deleted_rows: 0, - num_inserted_rows: 0, - num_updated_rows: 0, - num_attempts: 0, - num_rows: 0, - }); - } - - let merge_insert_response: MergeResult = - serde_json::from_str(&body).map_err(|e| Error::Http { - source: format!("Failed to parse merge_insert response: {}", e).into(), - request_id, - status_code: None, - })?; - - self.track_write_version(merge_insert_response.version); - Ok(merge_insert_response) } async fn set_unenforced_primary_key(&self, _columns: &[&str]) -> Result<()> { @@ -2862,20 +2814,20 @@ impl BaseTable for RemoteTable { write_params: lance::dataset::WriteParams, ) -> Result> { let overwrite = matches!(write_params.mode, lance::dataset::WriteMode::Overwrite); - Ok(Arc::new(insert::RemoteInsertExec::new( + Ok(Arc::new(insert::RemoteWriteExec::new( self.name.clone(), self.identifier.clone(), self.client.clone(), input, - overwrite, + WriteOp::Insert { overwrite }, None, self.branch.clone(), ))) } } -#[derive(Serialize)] -struct MergeInsertRequest { +#[derive(Serialize, Clone, Debug)] +pub(crate) struct MergeInsertRequest { on: String, when_matched_update_all: bool, when_matched_update_all_filt: Option, diff --git a/rust/lancedb/src/remote/table/insert.rs b/rust/lancedb/src/remote/table/insert.rs index 4b0aa5480..67ea7765d 100644 --- a/rust/lancedb/src/remote/table/insert.rs +++ b/rust/lancedb/src/remote/table/insert.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright The LanceDB Authors -//! DataFusion ExecutionPlan for inserting data into remote LanceDB tables. +//! DataFusion ExecutionPlan for streaming writes (add / merge_insert) to +//! remote LanceDB tables. use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -23,28 +24,57 @@ use lance::io::exec::utils::InstrumentedRecordBatchStreamAdapter; use crate::Error; use crate::remote::ARROW_STREAM_CONTENT_TYPE; use crate::remote::client::{HttpSend, RestfulLanceDbClient, Sender}; -use crate::remote::table::RemoteTable; -use crate::table::AddResult; +use crate::remote::table::{MergeInsertRequest, REQUEST_TIMEOUT_HEADER, RemoteTable}; use crate::table::datafusion::insert::COUNT_SCHEMA; use crate::table::write_progress::WriteProgressTracker; +use crate::table::{AddResult, MergeResult}; -/// ExecutionPlan for inserting data into a remote LanceDB table. +/// The write operation a [`RemoteWriteExec`] performs. Both variants share the +/// same Arrow-IPC streaming body and error side-channel; only the target +/// endpoint, query parameters, and parsed result type differ. +#[derive(Debug, Clone)] +pub(crate) enum WriteOp { + /// `add`: stream to `/v1/table/{id}/insert/`, optionally overwriting. + Insert { overwrite: bool }, + /// `merge_insert`: stream to `/v1/table/{id}/merge_insert/` with the merge + /// parameters carried as query params. Multipart is not supported for this + /// operation (the server has no multipart merge_insert endpoint), so an + /// `upload_id` combined with this op is a programming error. + MergeInsert { + query: MergeInsertRequest, + timeout: Option, + }, +} + +/// The parsed server response for a completed write, discriminated by the +/// operation that produced it. +#[derive(Debug, Clone)] +pub(crate) enum WriteResult { + Add(AddResult), + Merge(MergeResult), +} + +/// ExecutionPlan for streaming a write (add or merge_insert) to a remote +/// LanceDB table. /// -/// Streams data as Arrow IPC to `/v1/table/{id}/insert/` endpoint. +/// Streams data as Arrow IPC to the endpoint selected by [`WriteOp`]. Both +/// operations reuse the same error side-channel so an input stream error (e.g. +/// NaN rejection) surfaces with its original message rather than the masked +/// HTTP error Hyper produces when a request body stream fails under HTTP2. /// /// When `upload_id` is set, inserts are staged as part of a multipart write /// session and the plan supports multiple partitions for parallel uploads. /// Without `upload_id`, the plan requires a single partition and commits -/// immediately. +/// immediately. Multipart applies to `add` only. #[derive(Debug)] -pub struct RemoteInsertExec { +pub struct RemoteWriteExec { table_name: String, identifier: String, client: RestfulLanceDbClient, input: Arc, - overwrite: bool, + op: WriteOp, properties: Arc, - add_result: Arc>>, + result: Arc>>, metrics: ExecutionPlanMetricsSet, upload_id: Option, tracker: Option>, @@ -61,27 +91,28 @@ pub struct RemoteInsertExec { max_request_duration: Option, } -impl RemoteInsertExec { - /// Create a new single-partition RemoteInsertExec. +impl RemoteWriteExec { + /// Create a new single-partition RemoteWriteExec. pub fn new( table_name: String, identifier: String, client: RestfulLanceDbClient, input: Arc, - overwrite: bool, + op: WriteOp, tracker: Option>, branch: Option, ) -> Self { Self::new_inner( - table_name, identifier, client, input, overwrite, None, tracker, branch, None, None, + table_name, identifier, client, input, op, None, tracker, branch, None, None, ) } - /// Create a multi-partition RemoteInsertExec for use with multipart writes. + /// Create a multi-partition RemoteWriteExec for use with multipart writes. /// /// Each partition's insert is staged under the given `upload_id` without /// committing. The caller is responsible for calling the complete (or abort) - /// endpoint after all partitions finish. + /// endpoint after all partitions finish. Multipart is insert-only, so the + /// op is fixed to [`WriteOp::Insert`]. #[allow(clippy::too_many_arguments)] pub fn new_multipart( table_name: String, @@ -100,7 +131,7 @@ impl RemoteInsertExec { identifier, client, input, - overwrite, + WriteOp::Insert { overwrite }, Some(upload_id), tracker, branch, @@ -115,7 +146,7 @@ impl RemoteInsertExec { identifier: String, client: RestfulLanceDbClient, input: Arc, - overwrite: bool, + op: WriteOp, upload_id: Option, tracker: Option>, branch: Option, @@ -140,9 +171,9 @@ impl RemoteInsertExec { identifier, client, input, - overwrite, + op, properties: Arc::new(properties), - add_result: Arc::new(Mutex::new(None)), + result: Arc::new(Mutex::new(None)), metrics: ExecutionPlanMetricsSet::new(), upload_id, tracker, @@ -152,14 +183,30 @@ impl RemoteInsertExec { } } - /// Get the add result after execution. - // TODO: this will be used when we wire this up to Table::add(). - #[allow(dead_code)] + /// Get the add result after execution, if this exec ran an insert. pub fn add_result(&self) -> Option { - self.add_result + match self + .result .lock() .unwrap_or_else(|e| e.into_inner()) .clone() + { + Some(WriteResult::Add(r)) => Some(r), + _ => None, + } + } + + /// Get the merge result after execution, if this exec ran a merge_insert. + pub fn merge_result(&self) -> Option { + match self + .result + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone() + { + Some(WriteResult::Merge(r)) => Some(r), + _ => None, + } } /// Stream the input into an HTTP body as an Arrow IPC stream, capturing any @@ -464,24 +511,24 @@ impl PartRequestCtx<'_, S> { } } -impl DisplayAs for RemoteInsertExec { +impl DisplayAs for RemoteWriteExec { fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match t { DisplayFormatType::Default | DisplayFormatType::Verbose => { - write!( - f, - "RemoteInsertExec: table={}, overwrite={}", - self.table_name, self.overwrite - ) + write!(f, "RemoteWriteExec: table={}, op=", self.table_name)?; + match &self.op { + WriteOp::Insert { overwrite } => write!(f, "insert, overwrite={}", overwrite), + WriteOp::MergeInsert { .. } => write!(f, "merge_insert"), + } } DisplayFormatType::TreeRender => { - write!(f, "RemoteInsertExec") + write!(f, "RemoteWriteExec") } } } } -impl ExecutionPlan for RemoteInsertExec { +impl ExecutionPlan for RemoteWriteExec { fn name(&self) -> &str { Self::static_name() } @@ -516,15 +563,18 @@ impl ExecutionPlan for RemoteInsertExec { ) -> DataFusionResult> { if children.len() != 1 { return Err(DataFusionError::Internal( - "RemoteInsertExec requires exactly one child".to_string(), + "RemoteWriteExec requires exactly one child".to_string(), )); } + // Building a fresh exec (with a new, empty `result`) is what makes the + // outer rescannable retry loop work: `reset_state()` clears the captured + // result so a re-execution starts clean. Ok(Arc::new(Self::new_inner( self.table_name.clone(), self.identifier.clone(), self.client.clone(), children[0].clone(), - self.overwrite, + self.op.clone(), self.upload_id.clone(), self.tracker.clone(), self.branch.clone(), @@ -540,11 +590,19 @@ impl ExecutionPlan for RemoteInsertExec { ) -> DataFusionResult { if self.upload_id.is_none() && partition != 0 { return Err(DataFusionError::Internal( - "RemoteInsertExec only supports single partition execution without upload_id" + "RemoteWriteExec only supports single partition execution without upload_id" .to_string(), )); } + // Multipart is insert-only: the server has no multipart merge_insert + // endpoint, so a merge_insert with an upload_id is a programming error. + if self.upload_id.is_some() && matches!(self.op, WriteOp::MergeInsert { .. }) { + return Err(DataFusionError::Internal( + "merge_insert does not support multipart (upload_id) writes".to_string(), + )); + } + let input_stream = self.input.execute(partition, context)?; let input_schema = input_stream.schema(); let input_stream: SendableRecordBatchStream = @@ -556,8 +614,8 @@ impl ExecutionPlan for RemoteInsertExec { )); let client = self.client.clone(); let identifier = self.identifier.clone(); - let overwrite = self.overwrite; - let add_result = self.add_result.clone(); + let op = self.op.clone(); + let result_slot = self.result.clone(); let table_name = self.table_name.clone(); let upload_id = self.upload_id.clone(); let tracker = self.tracker.clone(); @@ -568,10 +626,12 @@ impl ExecutionPlan for RemoteInsertExec { let stream = futures::stream::once(async move { // Multipart writes with a byte budget split the partition into // several bounded, still-streamed requests so no single request - // stays open long enough to hit the client read timeout. + // stays open long enough to hit the client read timeout. This path + // is insert-only (guarded above). if let (Some(upload_id), Some(max_bytes)) = (upload_id.as_deref(), max_bytes_per_request) { + let overwrite = matches!(op, WriteOp::Insert { overwrite: true }); let ctx = PartRequestCtx { client: &client, identifier: &identifier, @@ -592,16 +652,36 @@ impl ExecutionPlan for RemoteInsertExec { )?); } - let mut request = client - .post(&format!("/v1/table/{}/insert/", identifier)) - .header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE); + // Build the request for the selected operation. Both endpoints take + // an Arrow-IPC streaming body and reuse the same error side-channel. + let mut request = match &op { + WriteOp::Insert { overwrite } => { + let mut request = client + .post(&format!("/v1/table/{}/insert/", identifier)) + .header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE); + if *overwrite { + request = request.query(&[("mode", "overwrite")]); + } + if let Some(ref uid) = upload_id { + request = request.query(&[("upload_id", uid.as_str())]); + } + request + } + WriteOp::MergeInsert { query, timeout } => { + let mut request = client + .post(&format!("/v1/table/{}/merge_insert/", identifier)) + .query(query) + .header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE); + if let Some(timeout) = timeout { + // (If it doesn't fit into u64, it's not worth sending anyways.) + if let Ok(timeout_ms) = u64::try_from(timeout.as_millis()) { + request = request.header(REQUEST_TIMEOUT_HEADER, timeout_ms); + } + } + request + } + }; - if overwrite { - request = request.query(&[("mode", "overwrite")]); - } - if let Some(ref uid) = upload_id { - request = request.query(&[("upload_id", uid.as_str())]); - } if let Some(ref b) = branch { request = request.query(&[("branch", b.as_str())]); } @@ -635,6 +715,8 @@ impl ExecutionPlan for RemoteInsertExec { // If the request failed due to an input stream error, surface the // original error (e.g. NaN rejection) instead of the HTTP error. + // This is the crux of the #2339 fix: Hyper silently swallows body + // stream errors under HTTP2, so we recover the original here. if let Ok(stream_err) = error_rx.try_recv() { return Err(stream_err); } @@ -642,7 +724,7 @@ impl ExecutionPlan for RemoteInsertExec { let (request_id, response) = result?; // For multipart writes, the staging response is not the final - // version. Only parse AddResult for non-multipart inserts. + // version. Only parse the result for non-multipart writes. if upload_id.is_none() { let body_text = response.text().await.map_err(|e| { DataFusionError::External(Box::new(Error::Http { @@ -652,21 +734,44 @@ impl ExecutionPlan for RemoteInsertExec { })) })?; - let parsed_result = if body_text.trim().is_empty() { - // Backward compatible with old servers - AddResult { version: 0 } - } else { - serde_json::from_str(&body_text).map_err(|e| { - DataFusionError::External(Box::new(Error::Http { - source: format!("Failed to parse add response: {}", e).into(), - request_id: request_id.clone(), - status_code: None, - })) - })? + let parsed_result = match &op { + WriteOp::Insert { .. } => { + let add = if body_text.trim().is_empty() { + // Backward compatible with old servers + AddResult { version: 0 } + } else { + serde_json::from_str(&body_text).map_err(|e| { + DataFusionError::External(Box::new(Error::Http { + source: format!("Failed to parse add response: {}", e).into(), + request_id: request_id.clone(), + status_code: None, + })) + })? + }; + WriteResult::Add(add) + } + WriteOp::MergeInsert { .. } => { + let merge = if body_text.trim().is_empty() { + // Backward compatible with old servers + MergeResult::default() + } else { + serde_json::from_str(&body_text).map_err(|e| { + DataFusionError::External(Box::new(Error::Http { + source: format!("Failed to parse merge_insert response: {}", e) + .into(), + request_id: request_id.clone(), + status_code: None, + })) + })? + }; + WriteResult::Merge(merge) + } }; - let mut res_lock = add_result.lock().map_err(|_| { - DataFusionError::Execution("Failed to acquire lock for add_result".to_string()) + let mut res_lock = result_slot.lock().map_err(|_| { + DataFusionError::Execution( + "Failed to acquire lock for write result".to_string(), + ) })?; *res_lock = Some(parsed_result); } else { @@ -680,7 +785,7 @@ impl ExecutionPlan for RemoteInsertExec { })?; } - // Return a single batch with count 0 (actual count is tracked in add_result) + // Return a single batch with count 0 (actual count is tracked in result) let count_array: ArrayRef = Arc::new(UInt64Array::from(vec![0u64])); let batch = RecordBatch::try_new(COUNT_SCHEMA.clone(), vec![count_array])?; Ok::<_, DataFusionError>(batch) @@ -711,9 +816,11 @@ mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; - use super::RemoteInsertExec; + use super::RemoteWriteExec; + use super::WriteOp; use crate::Table; use crate::remote::ARROW_STREAM_CONTENT_TYPE; + use crate::remote::table::MergeInsertRequest; use crate::table::datafusion::BaseTableAdapter; fn schema_json() -> &'static str { @@ -1028,7 +1135,7 @@ mod tests { let input = input_plan_from_batches(schema, batches).await; // A 1-byte budget forces every batch into its own part. - let exec = RemoteInsertExec::new_multipart( + let exec = RemoteWriteExec::new_multipart( "my_table".to_string(), "my_table".to_string(), client, @@ -1068,7 +1175,7 @@ mod tests { // A large byte budget and no time limit keep the whole partition in a // single part. - let exec = RemoteInsertExec::new_multipart( + let exec = RemoteWriteExec::new_multipart( "my_table".to_string(), "my_table".to_string(), client, @@ -1109,7 +1216,7 @@ mod tests { // A large byte budget but a tiny duration budget: writing and sending // one batch already takes longer than the limit, so each batch is cut // into its own part on the time check rather than the byte check. - let exec = RemoteInsertExec::new_multipart( + let exec = RemoteWriteExec::new_multipart( "my_table".to_string(), "my_table".to_string(), client, @@ -1144,7 +1251,7 @@ mod tests { // write relies on another partition having data to commit. let input = input_plan_from_batches(schema, vec![]).await; - let exec = RemoteInsertExec::new_multipart( + let exec = RemoteWriteExec::new_multipart( "my_table".to_string(), "my_table".to_string(), client, @@ -1184,7 +1291,7 @@ mod tests { let input = input_plan_from_batches(schema, batches).await; // A 1-byte budget forces every batch into its own part. - let exec = RemoteInsertExec::new_multipart( + let exec = RemoteWriteExec::new_multipart( "my_table".to_string(), "my_table".to_string(), client, @@ -1233,7 +1340,7 @@ mod tests { ]; let input = input_plan_from_partitions(schema, partitions).await; - let exec = RemoteInsertExec::new_multipart( + let exec = RemoteWriteExec::new_multipart( "my_table".to_string(), "my_table".to_string(), client, @@ -1267,7 +1374,7 @@ mod tests { // A large byte budget keeps the good batch and the following error in // the same part, exercising the mid-part abort path. let input: Arc = Arc::new(ErroringExec::new()); - let exec = RemoteInsertExec::new_multipart( + let exec = RemoteWriteExec::new_multipart( "my_table".to_string(), "my_table".to_string(), client, @@ -1297,6 +1404,66 @@ mod tests { ); } + #[tokio::test] + async fn test_merge_insert_input_error_surfaces_original() { + // Regression test for #2339 on the single-request merge_insert path. + // When the input stream errors mid-body, Hyper masks it under HTTP2 as a + // generic "stream error sent by user" message. The error side-channel + // must recover and surface the original DataFusion error instead. + use futures::StreamExt; + + let client = crate::remote::client::test_utils::client_with_handler(|request| { + assert_eq!(request.url().path(), "/v1/table/my_table/merge_insert/"); + http::Response::builder() + .status(200) + .body( + r#"{"version": 2, "num_updated_rows": 0, "num_inserted_rows": 0, "num_deleted_rows": 0}"# + .to_string(), + ) + .unwrap() + }); + + let query = MergeInsertRequest { + on: "id".to_string(), + when_matched_update_all: false, + when_matched_update_all_filt: None, + when_not_matched_insert_all: false, + when_not_matched_by_source_delete: false, + when_not_matched_by_source_delete_filt: None, + use_index: true, + use_lsm: None, + }; + + let input: Arc = Arc::new(ErroringExec::new()); + let exec = RemoteWriteExec::new( + "my_table".to_string(), + "my_table".to_string(), + client, + input, + WriteOp::MergeInsert { + query, + timeout: None, + }, + None, + None, + ); + + let mut stream = exec.execute(0, Arc::new(TaskContext::default())).unwrap(); + let mut err = None; + while let Some(item) = stream.next().await { + if let Err(e) = item { + err = Some(e); + break; + } + } + + let err = err.expect("expected the input stream error to surface"); + assert!( + err.to_string().contains("boom"), + "expected original input error, got: {err}" + ); + } + #[tokio::test] async fn test_multipart_records_progress_within_a_part() { use crate::table::write_progress::{ProgressCallback, WriteProgress, WriteProgressTracker}; @@ -1327,7 +1494,7 @@ mod tests { // A large byte budget keeps all three batches in one part; smooth // progress therefore requires bytes to be reported per chunk rather than // once when the part completes. - let exec = RemoteInsertExec::new_multipart( + let exec = RemoteWriteExec::new_multipart( "my_table".to_string(), "my_table".to_string(), client, From b505dc1315c41f05edf8b4d10ecadea316d31967 Mon Sep 17 00:00:00 2001 From: Joaquin Hui <132194176+joaquinhuigomez@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:56:43 +0100 Subject: [PATCH 18/29] fix: distinguish corrupt table from missing in open_table (#3731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `table_names()` lists any `*.lance` directory, but `open_table()` maps every `DatasetNotFound` to `TableNotFound`, so a corrupt or partially-written table looks identical to one that never existed (#3127). This takes the issue's Option 2: on `DatasetNotFound`, check the parent listing for the table's `.lance` entry — the same predicate `table_names()` uses — and return a new `TableCorrupted` error when the directory is present. The check runs only on the error path, and any failure in the recheck falls back to the previous `TableNotFound` behavior. Tests cover the reporter's empty-dir repro, a deleted-manifest case, true absence (still `TableNotFound`), and an end-to-end list-then-open assertion; the three new corrupt-case tests fail without the src change. `cargo test -p lancedb --lib` 732 passed, clippy/fmt clean, `cargo check --workspace --all-targets` clean (both language bindings end in wildcard error arms). Two notes for review: `Error` isn't `#[non_exhaustive]`, so the new variant is technically semver-breaking for exhaustive matchers (pre-1.0, and the alternative — changing `TableNotFound`'s shape — breaks more); and on the Python side corrupt tables now surface as `RuntimeError` rather than `ValueError`, which is the intended distinction but worth a maintainer's eye. `open_from_namespace` was left unchanged since namespace listings come from a server-side registry, not directory globbing. Closes #3127 --- rust/lancedb/src/connection.rs | 4 + rust/lancedb/src/error.rs | 4 + rust/lancedb/src/table.rs | 163 +++++++++++++++++++++++++++++++-- 3 files changed, 164 insertions(+), 7 deletions(-) diff --git a/rust/lancedb/src/connection.rs b/rust/lancedb/src/connection.rs index e0a4c22fa..f4ac0018b 100644 --- a/rust/lancedb/src/connection.rs +++ b/rust/lancedb/src/connection.rs @@ -456,6 +456,10 @@ impl Connection { /// /// # Returns /// Created [`TableRef`], or [`Error::TableNotFound`] if the table does not exist. + /// If the table's storage is present but holds no readable dataset (for example a + /// `.lance` directory left behind by an interrupted drop and re-create, which + /// [`Self::table_names`] still lists) this returns [`Error::TableCorrupted`] + /// instead. pub fn open_table(&self, name: impl Into) -> OpenTableBuilder { OpenTableBuilder::new( self.internal.clone(), diff --git a/rust/lancedb/src/error.rs b/rust/lancedb/src/error.rs index 37dfc44fc..9e4dd1f8c 100644 --- a/rust/lancedb/src/error.rs +++ b/rust/lancedb/src/error.rs @@ -18,6 +18,10 @@ pub enum Error { InvalidInput { message: String }, #[snafu(display("Table '{name}' was not found"))] TableNotFound { name: String, source: BoxError }, + #[snafu(display( + "Table '{name}' exists but could not be loaded (it may be corrupt or incomplete): {source}" + ))] + TableCorrupted { name: String, source: BoxError }, #[snafu(display("Database '{name}' was not found"))] DatabaseNotFound { name: String }, #[snafu(display("Database '{name}' already exists."))] diff --git a/rust/lancedb/src/table.rs b/rust/lancedb/src/table.rs index e0ab9754c..e0f12e2ff 100644 --- a/rust/lancedb/src/table.rs +++ b/rust/lancedb/src/table.rs @@ -50,6 +50,7 @@ use crate::DistanceType; use crate::blob::BlobRangeRequest; use crate::data::scannable::{PeekedScannable, Scannable, estimate_write_partitions}; use crate::database::Database; +use crate::database::listing::LANCE_FILE_EXTENSION; use crate::database::read_freshness::TableFreshness; use crate::embeddings::{EmbeddingDefinition, EmbeddingRegistry, MemoryRegistry}; use crate::error::{Error, Result}; @@ -146,6 +147,55 @@ pub(crate) fn map_namespace_lance_error(err: lance::Error, table_name: &str) -> } } +/// Map a `lance::Error::DatasetNotFound` for the table at `uri` into a `lancedb::Error`. +/// +/// Lance reports "there is nothing at this location" and "there is a table directory +/// here but nothing loadable inside it" with the same error. Only the first is a +/// `TableNotFound`: a `.lance` directory left behind by an interrupted drop and +/// re-create is still reported by `Connection::table_names`, so callers need to be able +/// to tell "never existed" from "exists but is broken". +/// +/// See . +async fn map_dataset_not_found( + uri: &str, + name: &str, + params: ReadParams, + err: lance::Error, +) -> Error { + let name = name.to_string(); + let source = Box::new(err); + if table_dir_exists(uri, params).await.unwrap_or(false) { + Error::TableCorrupted { name, source } + } else { + Error::TableNotFound { name, source } + } +} + +/// Whether a table directory is present at `uri`, even though no dataset could be +/// loaded from it. +/// +/// This looks for a `.lance` entry in the parent directory, which is exactly what +/// `ListingDatabase::table_names` lists, so the two APIs agree on whether a table is +/// present. Probing `uri` itself would not work: object stores have no empty +/// directories to probe, and on a local filesystem the interesting case is precisely an +/// empty directory. +async fn table_dir_exists(uri: &str, params: ReadParams) -> Result { + let (object_store, path, _) = DatasetBuilder::from_uri(uri) + .with_read_params(params) + .build_object_store() + .await?; + // Only `*.lance` entries are ever reported as tables, so nothing else can produce + // the list-then-open mismatch this guards against. + if path.extension() != Some(LANCE_FILE_EXTENSION) { + return Ok(false); + } + let (Some(parent), Some(dir_name)) = (path.parent(), path.filename()) else { + return Ok(false); + }; + let entries = object_store.read_dir(parent).await?; + Ok(entries.iter().any(|entry| entry.as_str() == dir_name)) +} + /// Defines the type of column #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ColumnKind { @@ -2240,6 +2290,8 @@ impl NativeTable { None => false, }; + // Kept so that a `DatasetNotFound` can be re-checked against storage below. + let recovery_params = params.clone(); let mut builder = DatasetBuilder::from_uri(uri).with_read_params(params); // Set up commit handler when managed_versioning is enabled @@ -2255,13 +2307,13 @@ impl NativeTable { builder = builder.with_commit_handler(commit_handler); } - let dataset = builder.load().await.map_err(|e| match e { - lance::Error::DatasetNotFound { .. } => Error::TableNotFound { - name: name.to_string(), - source: Box::new(e), - }, - e => e.into(), - })?; + let dataset = match builder.load().await { + Ok(dataset) => dataset, + Err(e @ lance::Error::DatasetNotFound { .. }) => { + return Err(map_dataset_not_found(uri, name, recovery_params, e).await); + } + Err(e) => return Err(e.into()), + }; let dataset = DatasetConsistencyWrapper::new_latest(dataset, read_consistency_interval); let id = Self::build_id(&namespace, name); @@ -3584,6 +3636,103 @@ mod tests { assert!(matches!(table.unwrap_err(), Error::TableNotFound { .. })); } + #[tokio::test] + async fn test_open_not_found_missing_lance_dir() { + let tmp_dir = tempdir().unwrap(); + let dataset_path = tmp_dir.path().join("test.lance"); + + let err = NativeTable::open(dataset_path.to_str().unwrap()) + .await + .unwrap_err(); + assert!( + matches!(&err, Error::TableNotFound { name, .. } if name == "test"), + "got {err:?}" + ); + } + + /// Write a table and then break it, leaving the `.lance` directory in place. + /// + /// `remove_all` reproduces an interrupted drop + re-create (the directory is left + /// empty); otherwise only the manifests are removed, leaving the data files behind. + async fn write_then_corrupt_table(dir: &std::path::Path, remove_all: bool) -> String { + let dataset_path = dir.join("test.lance"); + let uri = dataset_path.to_str().unwrap().to_string(); + + let batch = make_test_batches(); + let reader = RecordBatchIterator::new(vec![Ok(batch.clone())], batch.schema()); + Dataset::write(reader, &uri, None).await.unwrap(); + + if remove_all { + for entry in std::fs::read_dir(&dataset_path).unwrap() { + let entry = entry.unwrap(); + if entry.file_type().unwrap().is_dir() { + std::fs::remove_dir_all(entry.path()).unwrap(); + } else { + std::fs::remove_file(entry.path()).unwrap(); + } + } + assert_eq!(std::fs::read_dir(&dataset_path).unwrap().count(), 0); + } else { + let versions = dataset_path.join("_versions"); + assert!(versions.is_dir(), "expected manifests under {versions:?}"); + std::fs::remove_dir_all(&versions).unwrap(); + assert!(std::fs::read_dir(&dataset_path).unwrap().count() > 0); + } + + uri + } + + #[tokio::test] + async fn test_open_corrupt_empty_dir() { + let tmp_dir = tempdir().unwrap(); + let uri = write_then_corrupt_table(tmp_dir.path(), true).await; + + let err = NativeTable::open(&uri).await.unwrap_err(); + assert!( + matches!(&err, Error::TableCorrupted { name, .. } if name == "test"), + "got {err:?}" + ); + } + + #[tokio::test] + async fn test_open_corrupt_missing_manifest() { + let tmp_dir = tempdir().unwrap(); + let uri = write_then_corrupt_table(tmp_dir.path(), false).await; + + let err = NativeTable::open(&uri).await.unwrap_err(); + assert!( + matches!(&err, Error::TableCorrupted { name, .. } if name == "test"), + "got {err:?}" + ); + } + + /// A table listed by `table_names()` must not be reported as missing by + /// `open_table()`. See . + #[tokio::test] + async fn test_open_table_corrupt_is_still_listed() { + let tmp_dir = tempdir().unwrap(); + let db = connect(tmp_dir.path().to_str().unwrap()) + .execute() + .await + .unwrap(); + + write_then_corrupt_table(tmp_dir.path(), true).await; + + assert_eq!( + db.table_names().execute().await.unwrap(), + vec!["test".to_string()] + ); + let err = db.open_table("test").execute().await.unwrap_err(); + assert!( + matches!(&err, Error::TableCorrupted { name, .. } if name == "test"), + "got {err:?}" + ); + assert!( + err.to_string().contains("exists but could not be loaded"), + "got {err}" + ); + } + #[test] #[cfg(not(windows))] fn test_object_store_path() { From 77208fd464df045434d263b37a67e80a699ea739 Mon Sep 17 00:00:00 2001 From: Drew Gallardo Date: Thu, 30 Jul 2026 12:16:01 -0700 Subject: [PATCH 19/29] feat(remote): add RemoteTable fetch_blobs HTTP client (#3684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remote half of the blob read path. #3578 did local Python. This makes `RemoteTable` hit the server. - `fetch_blobs(column, row_ids or hits)` → bytes over `POST /v1/table/{id}/fetch_blobs/` - `blob_columns()` from the cached schema (describe already has the metadata, no extra route) - search then `fetch_blobs` works. row identity rides inside the blob descriptor so you do not need a public `_rowid` - `fetch_blob_files` still `NotSupported` on remote. use `fetch_blobs` for full bytes for now. Range is a follow up Accepts Binary / LargeBinary / BinaryView on the way back. Empty `row_ids` short-circuits. Version + branch go in the request body same as other read calls. ### Example ```python db = lancedb.connect(uri="db://my-project", api_key=...) table = db.open_table("clips") hits = table.search(query_vec).select(["id", "video"]).limit(10).to_arrow() # hits is just id + video. row ids are stashed on the descriptor blobs = table.fetch_blobs("video", hits) # null-aligned, same length as hits ``` Or pass ids yourself: ```python blobs = table.fetch_blobs("video", [10, 20, 30]) ``` ### Testing - `cargo test -p lancedb --features remote --lib` - `cargo test -p lancedb --features remote --test blob_integration` - `pytest python/tests/test_remote_db.py -k remote_blob` - live e2e against a local 0.5.0 remote server (search → fetch, nulls, nested path, old server gate) --------- Co-authored-by: Claude Opus 5 (1M context) --- python/python/lancedb/_blob.py | 29 +- python/python/lancedb/query.py | 11 +- python/python/lancedb/remote/table.py | 19 +- python/python/lancedb/table.py | 6 +- python/python/tests/test_remote_db.py | 182 ++++++- rust/lancedb/src/remote.rs | 1 - rust/lancedb/src/remote/db.rs | 52 ++ rust/lancedb/src/remote/table.rs | 637 ++++++++++++++++++++++++- rust/lancedb/src/remote/table/blobs.rs | 127 +++++ 9 files changed, 999 insertions(+), 65 deletions(-) create mode 100644 rust/lancedb/src/remote/table/blobs.rs diff --git a/python/python/lancedb/_blob.py b/python/python/lancedb/_blob.py index 338c973be..926769f48 100644 --- a/python/python/lancedb/_blob.py +++ b/python/python/lancedb/_blob.py @@ -14,14 +14,10 @@ import pyarrow as pa from .expr import Expr from .schema import blob_v2_column_paths from .types import BlobMode, QueryProjection, QueryProjectionSpec -from .util import get_uri_scheme if TYPE_CHECKING: from _typeshed import WriteableBuffer - from .remote.table import RemoteTable - from .table import AsyncTable, Table - BLOB_MODE_TO_HANDLING = { "lazy": "blobs_descriptions", "bytes": "all_binary", @@ -104,22 +100,6 @@ def validate_blob_mode(blob_mode: BlobMode) -> None: raise ValueError(f"blob_mode must be one of {modes}, got {blob_mode!r}") -def supports_blob_auto_row_id(table: Table | AsyncTable | RemoteTable) -> bool: - """Blob auto row-id applies to native tables, not LanceDB Cloud.""" - from .remote.table import RemoteTable - - if isinstance(table, RemoteTable): - return False - - inner = getattr(table, "_inner", None) - if inner is not None: - uri = inner.database().uri - if isinstance(uri, str) and get_uri_scheme(uri) == "db": - return False - - return True - - def projection_includes_blob_column( projection: QueryProjection, blob_columns: Iterable[str], @@ -164,16 +144,14 @@ def v2_projection_needs_row_id( def blob_auto_row_id_for_scan( - table: Table | AsyncTable | RemoteTable, schema: pa.Schema, projection: QueryProjection, *, with_row_id: bool | None, ) -> bool: + """Auto row-id only applies when the caller said nothing about row ids.""" if with_row_id is not None: return False - if not supports_blob_auto_row_id(table): - return False return v2_projection_needs_row_id(schema, projection, with_row_id=False) @@ -186,6 +164,11 @@ def finalize_blob_query_table( ) -> pa.Table: if user_requested_row_id or not blob_auto_row_id: return tbl + if "_rowid" not in tbl.column_names: + # A backend that ignores the row-id request leaves nothing to stash. Hand + # back the projection as-is so fetch_blobs raises the error that names the + # ways to supply row ids, rather than failing here about a hidden column. + return tbl return stash_auto_row_ids(tbl, blob_paths) diff --git a/python/python/lancedb/query.py b/python/python/lancedb/query.py index b85f50d7b..181eb9349 100644 --- a/python/python/lancedb/query.py +++ b/python/python/lancedb/query.py @@ -52,7 +52,6 @@ from ._blob import ( finalize_blob_query_table, replace_v2_blob_columns_with_bytes, replace_v2_blob_columns_with_bytes_sync, - supports_blob_auto_row_id, validate_blob_mode, ) from .types import BlobMode, QueryProjection @@ -1277,10 +1276,7 @@ class LanceQueryBuilder(ABC): return self._with_row_id is True def _blob_auto_row_id_enabled(self) -> bool: - if not supports_blob_auto_row_id(self._table): - return False return blob_auto_row_id_for_scan( - self._table, self._table.schema, self._columns, with_row_id=self._with_row_id, @@ -2771,7 +2767,7 @@ class AsyncQueryBase(object): ) async def _maybe_add_blob_row_id(self) -> None: - if self._table is None or not supports_blob_auto_row_id(self._table): + if self._table is None: self._blob_auto_row_id = False self._blob_paths = () return @@ -2779,7 +2775,6 @@ class AsyncQueryBase(object): req = self._inner.to_query_request() schema = await self._table.schema() self._blob_auto_row_id = blob_auto_row_id_for_scan( - self._table, schema, req.select, with_row_id=self._with_row_id, @@ -3031,7 +3026,6 @@ class AsyncQueryBase(object): schema = await self._table.schema() blob_auto_row_id = blob_auto_row_id_for_scan( - self._table, schema, query.columns, with_row_id=self._with_row_id, @@ -3875,10 +3869,9 @@ class AsyncHybridQuery(AsyncStandardQuery, AsyncVectorQueryBase): req = fts_query._inner.to_query_request() blob_auto_row_id = False blob_paths: tuple[str, ...] = () - if self._table is not None and supports_blob_auto_row_id(self._table): + if self._table is not None: schema = await self._table.schema() blob_auto_row_id = blob_auto_row_id_for_scan( - self._table, schema, req.select, with_row_id=self._with_row_id, diff --git a/python/python/lancedb/remote/table.py b/python/python/lancedb/remote/table.py index 01b90c019..20a952c53 100644 --- a/python/python/lancedb/remote/table.py +++ b/python/python/lancedb/remote/table.py @@ -20,6 +20,7 @@ from typing import ( import warnings from lancedb import __version__ +from lancedb._blob import BlobFile from lancedb._lancedb import ( AddColumnsResult, @@ -1037,22 +1038,22 @@ class RemoteTable(Table): ) def blob_columns(self) -> list[str]: - raise NotImplementedError( - "blob_columns() is not yet supported on the LanceDB Cloud" - ) + return LOOP.run(self._table.blob_columns()) - def fetch_blobs(self, column: str, row_ids) -> pa.LargeBinaryArray: - raise NotImplementedError("fetch_blobs() is not supported on LanceDB Cloud") + def fetch_blobs( + self, column: str, row_ids: Union[list[int], pa.Table] + ) -> pa.LargeBinaryArray: + return LOOP.run(self._table.fetch_blobs(column, row_ids)) def fetch_blob_ranges(self, column: str, requests) -> pa.LargeBinaryArray: raise NotImplementedError( "fetch_blob_ranges() is not supported on LanceDB Cloud" ) - def fetch_blob_files(self, column: str, row_ids): - raise NotImplementedError( - "fetch_blob_files() is not supported on LanceDB Cloud" - ) + def fetch_blob_files( + self, column: str, row_ids: Union[list[int], pa.Table] + ) -> "list[Optional[BlobFile]]": + return LOOP.run(self._table.fetch_blob_files(column, row_ids)) def head(self, n=5) -> pa.Table: """ diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index 56764bfd3..26d78a427 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -1574,8 +1574,10 @@ class Table(ABC): """Open lazy, seekable :class:`~lancedb._blob.BlobFile` handles. Prefer this over :meth:`fetch_blobs` for large payloads. ``row_ids`` is - a ``list[int]`` or query ``pyarrow.Table`` with ``_rowid`` (or stashed - row-id metadata). Null rows are ``None``. Local tables only. + a ``list[int]`` or a query ``pyarrow.Table`` carrying row identity via + ``_rowid`` or a ``_lance_row_id`` field on the blob descriptor. Null + rows are ``None``. Unsupported on LanceDB Cloud, where + :meth:`fetch_blobs` returns full bytes instead. """ @abstractmethod diff --git a/python/python/tests/test_remote_db.py b/python/python/tests/test_remote_db.py index 55718f516..dd46628f1 100644 --- a/python/python/tests/test_remote_db.py +++ b/python/python/tests/test_remote_db.py @@ -1020,7 +1020,7 @@ def query_test_table(query_handler, *, server_version=Version("0.1.0")): request.send_header("Content-Type", "application/json") request.send_header("phalanx-version", str(server_version)) request.end_headers() - request.wfile.write(b"{}") + request.wfile.write(b'{"version": 1, "schema": {"fields": []}}') elif request.path == "/v1/table/test/query/": content_len = int(request.headers.get("Content-Length")) body = request.rfile.read(content_len) @@ -1858,3 +1858,183 @@ def test_inherited_remote_table_reopens_after_fork(): finally: server.shutdown() server_thread.join() + + +BLOB_DESCRIBE_RESPONSE = { + "table": "test", + "version": 1, + "schema": { + "fields": [ + {"name": "id", "type": {"type": "int64"}, "nullable": False}, + { + "name": "image", + "type": { + "type": "struct", + "fields": [ + { + "name": "data", + "type": {"type": "large_binary"}, + "nullable": True, + }, + {"name": "uri", "type": {"type": "string"}, "nullable": True}, + ], + }, + "nullable": True, + "metadata": { + "ARROW:extension:name": "lance.blob.v2", + "ARROW:extension:metadata": "", + }, + }, + ] + }, +} + + +def blob_query_response_table(): + image_field = pa.field( + "image", + pa.struct( + [ + pa.field("kind", pa.uint8(), nullable=False), + pa.field("position", pa.uint64(), nullable=False), + pa.field("size", pa.uint64(), nullable=False), + pa.field("blob_id", pa.uint32(), nullable=False), + pa.field("blob_uri", pa.string(), nullable=False), + ] + ), + metadata={"lance-encoding:blob": "true"}, + ) + images = pa.StructArray.from_arrays( + [ + pa.array([1, 0, 0], type=pa.uint8()), + pa.array([0, 0, 0], type=pa.uint64()), + pa.array([5, 0, 5], type=pa.uint64()), + pa.array([1, 0, 2], type=pa.uint32()), + pa.array(["", "", ""], type=pa.string()), + ], + fields=image_field.type, + mask=pa.array([False, True, False]), + ) + return pa.Table.from_arrays( + [ + pa.array([1, 2, 3], type=pa.int64()), + images, + pa.array([10, 20, 30], type=pa.uint64()), + ], + schema=pa.schema( + [ + pa.field("id", pa.int64(), nullable=False), + image_field, + pa.field("_rowid", pa.uint64()), + ] + ), + ) + + +@contextlib.contextmanager +def blob_remote_table(*, server_version=Version("0.5.0")): + def handler(request): + if request.path == "/v1/table/test/describe/": + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.send_header("phalanx-version", str(server_version)) + request.end_headers() + request.wfile.write(json.dumps(BLOB_DESCRIBE_RESPONSE).encode()) + elif request.path == "/v1/table/test/query/": + content_len = int(request.headers.get("Content-Length", 0)) + body = json.loads(request.rfile.read(content_len)) + assert body["columns"] == ["id", "image"] + assert body["with_row_id"] is True + response_table = blob_query_response_table() + request.send_response(200) + request.send_header("Content-Type", "application/vnd.apache.arrow.file") + request.end_headers() + with pa.ipc.new_file(request.wfile, response_table.schema) as writer: + writer.write_table(response_table) + elif request.path == "/v1/table/test/fetch_blobs/": + content_len = int(request.headers.get("Content-Length", 0)) + body = json.loads(request.rfile.read(content_len)) + assert body["column"] == "image" + assert body["row_ids"] == [10, 20, 30] + response_table = pa.table( + {"image": pa.array([b"alpha", None, b"gamma"], type=pa.large_binary())} + ) + request.send_response(200) + request.send_header("Content-Type", "application/vnd.apache.arrow.stream") + request.end_headers() + with pa.ipc.new_stream(request.wfile, response_table.schema) as writer: + writer.write_table(response_table) + else: + request.send_response(404) + request.end_headers() + + with mock_lancedb_connection(handler) as db: + yield db.open_table("test") + + +def test_remote_blob_columns_and_fetch(): + with blob_remote_table() as table: + assert table.blob_columns() == ["image"] + blobs = table.fetch_blobs("image", [10, 20, 30]) + assert blobs.to_pylist() == [b"alpha", None, b"gamma"] + with pytest.raises(NotImplementedError, match="Use fetch_blobs for full bytes"): + table.fetch_blob_files("image", [10, 20, 30]) + + +def test_remote_blob_fetch_accepts_query_table(): + hits = pa.table({"_rowid": pa.array([10, 20, 30], type=pa.uint64())}) + + with blob_remote_table() as table: + blobs = table.fetch_blobs("image", hits) + + assert blobs.to_pylist() == [b"alpha", None, b"gamma"] + + +def test_remote_blob_query_stashes_row_ids_for_fetch(): + with blob_remote_table() as table: + hits = table.search().select(["id", "image"]).limit(3).to_arrow() + assert "_rowid" not in hits.column_names + assert "_lance_row_id" in hits.schema.field("image").type.names + blobs = table.fetch_blobs("image", hits) + + assert blobs.to_pylist() == [b"alpha", None, b"gamma"] + + +def test_remote_blob_query_survives_a_server_that_ignores_the_row_id_request(): + def handler(request): + if request.path == "/v1/table/test/describe/": + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.send_header("phalanx-version", "0.5.0") + request.end_headers() + request.wfile.write(json.dumps(BLOB_DESCRIBE_RESPONSE).encode()) + elif request.path == "/v1/table/test/query/": + content_len = int(request.headers.get("Content-Length", 0)) + assert json.loads(request.rfile.read(content_len))["with_row_id"] is True + response_table = blob_query_response_table().drop_columns(["_rowid"]) + request.send_response(200) + request.send_header("Content-Type", "application/vnd.apache.arrow.file") + request.end_headers() + with pa.ipc.new_file(request.wfile, response_table.schema) as writer: + writer.write_table(response_table) + else: + request.send_response(404) + request.end_headers() + + with mock_lancedb_connection(handler) as db: + table = db.open_table("test") + hits = table.search().select(["id", "image"]).limit(3).to_arrow() + + assert hits.column_names == ["id", "image"] + assert "_lance_row_id" not in hits.schema.field("image").type.names + with pytest.raises(ValueError, match="pass a list of row ids"): + table.fetch_blobs("image", hits) + + +def test_remote_blob_byte_apis_not_supported_on_old_server(): + with blob_remote_table(server_version=Version("0.1.0")) as table: + assert table.blob_columns() == ["image"] + with pytest.raises(NotImplementedError, match="not supported"): + table.fetch_blobs("image", [1]) + with pytest.raises(NotImplementedError, match="not supported"): + table.fetch_blob_files("image", [1]) diff --git a/rust/lancedb/src/remote.rs b/rust/lancedb/src/remote.rs index 25e19c537..657ae1a47 100644 --- a/rust/lancedb/src/remote.rs +++ b/rust/lancedb/src/remote.rs @@ -14,7 +14,6 @@ pub(crate) mod table; pub(crate) mod util; const ARROW_STREAM_CONTENT_TYPE: &str = "application/vnd.apache.arrow.stream"; -#[cfg(test)] const ARROW_FILE_CONTENT_TYPE: &str = "application/vnd.apache.arrow.file"; #[cfg(test)] const JSON_CONTENT_TYPE: &str = "application/json"; diff --git a/rust/lancedb/src/remote/db.rs b/rust/lancedb/src/remote/db.rs index 25fcd23b1..a67f9dbb3 100644 --- a/rust/lancedb/src/remote/db.rs +++ b/rust/lancedb/src/remote/db.rs @@ -79,6 +79,10 @@ impl ServerVersion { pub fn support_multipart_write(&self) -> bool { self.0 >= semver::Version::new(0, 4, 0) } + + pub fn support_blobs(&self) -> bool { + self.0 >= semver::Version::new(0, 5, 0) + } } pub const OPT_REMOTE_PREFIX: &str = "remote_database_"; @@ -661,6 +665,7 @@ impl Database for RemoteDatabase { RemoteTable::::handle_table_not_found(&request.name, rsp, &request_id).await?; let rsp = self.client.check_response(&request_id, rsp).await?; let version = parse_server_version(&request_id, &rsp)?; + let describe_body = rsp.text().await.ok(); let table_identifier = build_table_identifier( &request.name, &request.namespace_path, @@ -673,6 +678,12 @@ impl Database for RemoteDatabase { table_identifier, version, )); + // This describe already carries the schema, so hand it to the table + // instead of making the first schema read fetch it again. A version or + // branch pin applied after this invalidates the cache. + if let Some(body) = &describe_body { + table.seed_schema(body); + } let cache_key = build_cache_key(&request.name, &request.namespace_path); self.table_cache.insert(cache_key, table.clone()).await; Ok(table) @@ -923,6 +934,7 @@ impl From for RemoteOptions { mod tests { use super::{NamespaceHeaderProviderContext, build_cache_key}; use std::collections::HashMap; + use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, OnceLock}; use arrow_array::{Int32Array, RecordBatch}; @@ -1073,6 +1085,46 @@ mod tests { assert_eq!(table.name(), "table1"); } + #[tokio::test] + async fn test_open_table_seeds_the_schema_from_its_describe() { + let describe_calls = Arc::new(AtomicUsize::new(0)); + let counted = describe_calls.clone(); + let conn = Connection::new_with_handler(move |request| { + assert_eq!(request.url().path(), "/v1/table/table1/describe/"); + counted.fetch_add(1, Ordering::SeqCst); + http::Response::builder() + .status(200) + .body( + r#"{"version": 1, "schema": {"fields": [ + {"name": "id", "type": {"type": "int64"}, "nullable": false} + ]}}"# + .to_string(), + ) + .unwrap() + }); + + let table = conn.open_table("table1").execute().await.unwrap(); + let schema = table.schema().await.unwrap(); + + assert_eq!(schema.field(0).name(), "id"); + assert_eq!(describe_calls.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn test_open_table_survives_a_describe_body_it_cannot_parse() { + let conn = Connection::new_with_handler(|request| { + assert_eq!(request.url().path(), "/v1/table/table1/describe/"); + http::Response::builder() + .status(200) + .body(r#"{"table": "table1"}"#.to_string()) + .unwrap() + }); + + let table = conn.open_table("table1").execute().await.unwrap(); + + assert_eq!(table.name(), "table1"); + } + #[tokio::test] async fn test_open_table_branch_and_version() { let conn = Connection::new_with_handler(|request| { diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index b0d7dacc5..b668f6d67 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -1,20 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright The LanceDB Authors +mod blobs; pub mod insert; use self::insert::{RemoteWriteExec, WriteOp}; -use crate::expr::expr_to_sql_string; -use crate::table::write_progress::FinishOnDrop; -// Used by the test module below (single-request write requests set these -// headers directly in test handlers); kept at module scope so both the -// library and its tests can name them. -#[cfg(test)] -use super::ARROW_STREAM_CONTENT_TYPE; use super::client::RequestResultExt; use super::client::{HttpSend, RestfulLanceDbClient, Sender}; use super::db::ServerVersion; +use super::{ARROW_FILE_CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE}; +use crate::blob::BlobFile; use crate::data::scannable::{PeekedScannable, Scannable, estimate_write_partitions}; +use crate::expr::expr_to_sql_string; use crate::index::Index; use crate::index::IndexStatistics; use crate::index::waiter::wait_for_index; @@ -31,6 +28,7 @@ use crate::table::Tags; use crate::table::UpdateResult; use crate::table::merge::MergeFilter; use crate::table::query::create_multi_vector_plan; +use crate::table::write_progress::FinishOnDrop; use crate::table::{AlterColumnsResult, FieldMetadataUpdate, UpdateFieldMetadataResult}; use crate::table::{AnyQuery, Filter, Predicate, PreprocessingOutput, TableStatistics}; use crate::utils::background_cache::BackgroundCache; @@ -47,16 +45,15 @@ use crate::{ merge::MergeInsertBuilder, }, }; -use arrow_array::RecordBatchReader; -use arrow_ipc::reader::FileReader; -use arrow_schema::{DataType, SchemaRef}; +use arrow_array::{LargeBinaryArray, RecordBatch, RecordBatchReader}; +use arrow_ipc::reader::{FileReader, StreamReader}; +use arrow_schema::{ArrowError, DataType, SchemaRef}; use async_trait::async_trait; use chrono::{DateTime, Utc}; use datafusion_common::DataFusionError; use datafusion_physical_plan::stream::RecordBatchStreamAdapter; use datafusion_physical_plan::{ExecutionPlan, RecordBatchStream, SendableRecordBatchStream}; use futures::{StreamExt, TryStreamExt}; -#[cfg(test)] use http::header::CONTENT_TYPE; use http::{HeaderName, StatusCode}; use lance::arrow::json::{JsonDataType, JsonSchema}; @@ -314,6 +311,20 @@ impl RemoteTable { } } + /// Seed the schema cache from a `describe` body the caller already fetched. + /// + /// Best effort. `open_table` succeeds today without reading this body, so a + /// body we cannot parse leaves the cache empty and the next schema read + /// fetches it again through the path that reports a real error. + pub(crate) fn seed_schema(&self, describe_body: &str) { + let Ok(description) = serde_json::from_str::(describe_body) else { + return; + }; + if let Ok(schema) = arrow_schema::Schema::try_from(description.schema) { + self.schema_cache.seed(Arc::new(schema)); + } + } + /// Return a new handle scoped to `branch`, sharing the client but with fresh /// caches and version/freshness state (the branch tracks its own latest). /// Mirrors `NativeTable`'s handle-per-branch model. @@ -526,19 +537,39 @@ impl RemoteTable { result } - async fn read_arrow_stream( + async fn read_arrow_response( &self, request_id: &str, response: reqwest::Response, ) -> Result { let response = self.check_table_response(request_id, response).await?; - // There isn't a way to actually stream this data yet. I have an upstream issue: - // https://github.com/apache/arrow-rs/issues/6420 + // The header has to be read before the body, which consumes the response. + let content_type = response + .headers() + .get(CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .map(str::to_owned); + let framing = resolve_arrow_ipc_framing(content_type.as_deref(), request_id)?; + + // Buffer the whole body. File framing keeps its footer at the end, so /query + // cannot decode incrementally. Stream framing could, via + // arrow_ipc::reader::StreamDecoder, but fetch_blobs concatenates every batch + // before returning, so no caller would see data sooner. let body = response.bytes().await.err_to_http(request_id.into())?; - let reader = FileReader::try_new(Cursor::new(body), None)?; - let schema = reader.schema(); - let stream = futures::stream::iter(reader).map_err(DataFusionError::from); + type IpcBatchIterator = + Box> + Send>; + let (schema, batches): (SchemaRef, IpcBatchIterator) = match framing { + ArrowIpcFraming::Stream => { + let reader = StreamReader::try_new(Cursor::new(body), None)?; + (reader.schema(), Box::new(reader)) + } + ArrowIpcFraming::File => { + let reader = FileReader::try_new(Cursor::new(body), None)?; + (reader.schema(), Box::new(reader)) + } + }; + let stream = futures::stream::iter(batches).map_err(DataFusionError::from); Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream))) } @@ -910,7 +941,7 @@ impl RemoteTable { let futures = requests.into_iter().map(|req| async move { let (request_id, response) = self.send(req, true).await?; self.track_read_version_from_headers(response.headers()); - self.read_arrow_stream(&request_id, response).await + self.read_arrow_response(&request_id, response).await }); let streams = futures::future::try_join_all(futures); @@ -976,6 +1007,47 @@ struct TableDescription { location: Option, } +/// How a response body frames its Arrow IPC payload. `/query` answers with file framing +/// and `fetch_blobs` with stream framing, so the reader is picked per response. +enum ArrowIpcFraming { + File, + Stream, +} + +/// A response with no `Content-Type` uses file framing, preserving this helper's +/// behavior before `fetch_blobs` introduced stream responses. +fn resolve_arrow_ipc_framing( + content_type: Option<&str>, + request_id: &str, +) -> Result { + let Some(media_type) = content_type.map(base_media_type) else { + return Ok(ArrowIpcFraming::File); + }; + if media_type.eq_ignore_ascii_case(ARROW_STREAM_CONTENT_TYPE) { + return Ok(ArrowIpcFraming::Stream); + } + if media_type.eq_ignore_ascii_case(ARROW_FILE_CONTENT_TYPE) { + return Ok(ArrowIpcFraming::File); + } + Err(Error::Http { + source: format!( + "Expected an Arrow IPC response with Content-Type '{ARROW_STREAM_CONTENT_TYPE}' \ + or '{ARROW_FILE_CONTENT_TYPE}', got '{media_type}'" + ) + .into(), + request_id: request_id.into(), + status_code: None, + }) +} + +/// Strip media-type parameters before matching against the Arrow content types. +fn base_media_type(content_type: &str) -> &str { + match content_type.split_once(';') { + Some((media_type, _parameters)) => media_type.trim(), + None => content_type.trim(), + } +} + /// Extract an Error from Arc, reconstructing if the Arc is shared. /// This is needed because `Shared` futures cache results internally, so /// `Arc::try_unwrap` typically fails. @@ -2008,6 +2080,22 @@ impl BaseTable for RemoteTable { } } + async fn blob_columns(&self) -> Result> { + self.blob_columns_impl().await + } + + async fn fetch_blobs(&self, column: &str, row_ids: &[u64]) -> Result { + self.fetch_blobs_impl(column, row_ids).await + } + + async fn fetch_blob_files( + &self, + column: &str, + row_ids: &[u64], + ) -> Result>> { + self.fetch_blob_files_impl(column, row_ids).await + } + async fn explain_plan(&self, query: &AnyQuery, verbose: bool) -> Result { let base_request = self.post_read(&format!("/v1/table/{}/explain_plan/", self.identifier)); @@ -2911,7 +2999,9 @@ mod tests { use crate::table::{AddDataMode, FieldMetadataUpdate, FtsToken}; use arrow::{array::AsArray, compute::concat_batches, datatypes::Int32Type}; - use arrow_array::{Int32Array, RecordBatch, RecordBatchIterator, record_batch}; + use arrow_array::Array; + use arrow_array::builder::LargeBinaryBuilder; + use arrow_array::{BinaryArray, Int32Array, RecordBatch, RecordBatchIterator, record_batch}; use arrow_schema::{DataType, Field, Schema}; use chrono::{DateTime, Utc}; use futures::{StreamExt, TryFutureExt, future::BoxFuture}; @@ -2935,7 +3025,6 @@ mod tests { AnalyzePlanDistributedMetrics, ColumnOrdering, ExecutableQuery, QueryBase, QueryExecutionOptions, }, - remote::ARROW_FILE_CONTENT_TYPE, }; #[tokio::test] @@ -3117,6 +3206,17 @@ mod tests { body } + fn write_ipc_stream_uncompressed(data: &RecordBatch) -> Vec { + let mut body = Vec::new(); + { + let mut writer = arrow_ipc::writer::StreamWriter::try_new(&mut body, &data.schema()) + .expect("Failed to create writer"); + writer.write(data).expect("Failed to write data"); + writer.finish().expect("Failed to finish"); + } + body + } + fn write_ipc_file(data: &RecordBatch) -> Vec { let mut body = Vec::new(); { @@ -3788,6 +3888,503 @@ mod tests { assert_eq!(data[0].as_ref().unwrap(), &expected_data); } + fn blob_describe_response() -> http::Response { + let schema = Schema::new(vec![ + Field::new("id", DataType::Int64, false), + crate::blob("image", true), + Field::new("caption", DataType::Utf8, true), + crate::blob("thumbnail", true), + ]); + let json_schema = JsonSchema::try_from(&schema).unwrap(); + http::Response::builder() + .status(200) + .body(serde_json::json!({ "version": 1, "schema": json_schema }).to_string()) + .unwrap() + } + + #[rstest] + #[case(semver::Version::new(0, 1, 0))] + #[case(semver::Version::new(0, 5, 0))] + #[tokio::test] + async fn test_blob_columns_read_the_schema_on_any_server_version( + #[case] version: semver::Version, + ) { + let table = Table::new_with_handler_version("my_table", version, |request| { + assert_eq!(request.url().path(), "/v1/table/my_table/describe/"); + blob_describe_response() + }); + + let columns = table.blob_columns().await.unwrap(); + assert_eq!(columns, vec!["image".to_string(), "thumbnail".to_string()]); + } + + #[tokio::test] + async fn test_fetch_blobs_decodes_null_aligned_bytes() { + let mut builder = LargeBinaryBuilder::new(); + builder.append_value(b"alpha"); + builder.append_null(); + builder.append_value(b"gamma"); + let blobs = builder.finish(); + let expected = RecordBatch::try_new( + Arc::new(Schema::new(vec![Field::new( + "image", + DataType::LargeBinary, + true, + )])), + vec![Arc::new(blobs)], + ) + .unwrap(); + let expected_ref = expected.clone(); + + let table = Table::new_with_handler_version( + "my_table", + semver::Version::new(0, 5, 0), + move |request| { + assert_eq!(request.method(), "POST"); + assert_eq!(request.url().path(), "/v1/table/my_table/fetch_blobs/"); + let body: serde_json::Value = + serde_json::from_slice(request.body().unwrap().as_bytes().unwrap()).unwrap(); + assert_eq!(body["column"], "image"); + assert_eq!(body["row_ids"], serde_json::json!([10, 20, 30])); + + http::Response::builder() + .status(200) + .header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE) + .body(write_ipc_stream_uncompressed(&expected_ref)) + .unwrap() + }, + ); + + let blobs = table.fetch_blobs("image", &[10, 20, 30]).await.unwrap(); + assert_eq!(blobs.len(), 3); + assert_eq!(blobs.value(0), b"alpha"); + assert!(blobs.is_null(1)); + assert_eq!(blobs.value(2), b"gamma"); + } + + #[tokio::test] + async fn test_fetch_blobs_concatenates_multiple_batches() { + let schema = Arc::new(Schema::new(vec![Field::new( + "image", + DataType::LargeBinary, + true, + )])); + + let mut first_builder = LargeBinaryBuilder::new(); + first_builder.append_value(b"alpha"); + first_builder.append_null(); + let first_batch = + RecordBatch::try_new(schema.clone(), vec![Arc::new(first_builder.finish())]).unwrap(); + + let mut second_builder = LargeBinaryBuilder::new(); + second_builder.append_value(b"gamma"); + let second_batch = + RecordBatch::try_new(schema.clone(), vec![Arc::new(second_builder.finish())]).unwrap(); + + let mut body = Vec::new(); + { + let mut writer = arrow_ipc::writer::StreamWriter::try_new(&mut body, &schema).unwrap(); + writer.write(&first_batch).unwrap(); + writer.write(&second_batch).unwrap(); + writer.finish().unwrap(); + } + + let table = Table::new_with_handler_version( + "my_table", + semver::Version::new(0, 5, 0), + move |request| { + assert_eq!(request.url().path(), "/v1/table/my_table/fetch_blobs/"); + http::Response::builder() + .status(200) + .header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE) + .body(body.clone()) + .unwrap() + }, + ); + + let blobs = table.fetch_blobs("image", &[10, 20, 30]).await.unwrap(); + assert_eq!(blobs.len(), 3); + assert_eq!(blobs.value(0), b"alpha"); + assert!(blobs.is_null(1)); + assert_eq!(blobs.value(2), b"gamma"); + } + + fn table_with_fetch_blobs_response(body: Vec) -> Table { + table_with_fetch_blobs_content_type(Some(ARROW_STREAM_CONTENT_TYPE), body) + } + + fn table_with_fetch_blobs_content_type( + content_type: Option<&'static str>, + body: Vec, + ) -> Table { + Table::new_with_handler_version("my_table", semver::Version::new(0, 5, 0), move |request| { + assert_eq!(request.url().path(), "/v1/table/my_table/fetch_blobs/"); + let mut response = http::Response::builder().status(200); + if let Some(content_type) = content_type { + response = response.header(CONTENT_TYPE, content_type); + } + response.body(body.clone()).unwrap() + }) + } + + fn one_row_blob_batch(column: &str) -> RecordBatch { + let mut builder = LargeBinaryBuilder::new(); + builder.append_value(b"alpha"); + RecordBatch::try_new( + Arc::new(Schema::new(vec![Field::new( + column, + DataType::LargeBinary, + true, + )])), + vec![Arc::new(builder.finish())], + ) + .unwrap() + } + + fn one_row_blob_ipc_stream(column: &str) -> Vec { + write_ipc_stream_uncompressed(&one_row_blob_batch(column)) + } + + #[tokio::test] + async fn test_fetch_blobs_sends_the_checked_out_version() { + let ipc = one_row_blob_ipc_stream("image"); + let table = Table::new_with_handler_version( + "my_table", + semver::Version::new(0, 5, 0), + move |request| match request.url().path() { + "/v1/table/my_table/describe/" => http::Response::builder() + .status(200) + .body(br#"{"version": 42, "schema": {"fields": []}}"#.to_vec()) + .unwrap(), + "/v1/table/my_table/fetch_blobs/" => { + let body = request_body_json(&request); + assert_eq!( + body["version"], 42, + "blob reads must use the same snapshot as the query" + ); + http::Response::builder() + .status(200) + .header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE) + .body(ipc.clone()) + .unwrap() + } + path => panic!("unexpected request path: {path}"), + }, + ); + + table.checkout(42).await.unwrap(); + + assert_eq!(table.fetch_blobs("image", &[10]).await.unwrap().len(), 1); + } + + #[tokio::test] + async fn test_fetch_blobs_sends_the_checked_out_branch() { + use lance::dataset::refs::Ref; + let ipc = one_row_blob_ipc_stream("image"); + let table = Table::new_with_handler_version( + "my_table", + semver::Version::new(0, 5, 0), + move |request| match request.url().path() { + "/v1/table/my_table/branches/create/" => http::Response::builder() + .status(200) + .body(b"{}".to_vec()) + .unwrap(), + "/v1/table/my_table/fetch_blobs/" => { + let body = request_body_json(&request); + assert_eq!(body["branch"], "exp", "blob reads must stay on the branch"); + http::Response::builder() + .status(200) + .header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE) + .body(ipc.clone()) + .unwrap() + } + path => panic!("unexpected request path: {path}"), + }, + ); + + let branch = table + .create_branch("exp", Ref::Version(None, None)) + .await + .unwrap(); + + assert_eq!(branch.fetch_blobs("image", &[10]).await.unwrap().len(), 1); + } + + #[tokio::test] + async fn test_fetch_blobs_sends_a_nested_column_as_a_dotted_path() { + let ipc = one_row_blob_ipc_stream("info.blob"); + let table = Table::new_with_handler_version( + "my_table", + semver::Version::new(0, 5, 0), + move |request| { + let body = request_body_json(&request); + assert_eq!(body["column"], "info.blob"); + http::Response::builder() + .status(200) + .header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE) + .body(ipc.clone()) + .unwrap() + }, + ); + + let blobs = table.fetch_blobs("info.blob", &[10]).await.unwrap(); + + assert_eq!(blobs.value(0), b"alpha"); + } + + fn write_empty_ipc_stream(schema: &Schema) -> Vec { + let mut body = Vec::new(); + arrow_ipc::writer::StreamWriter::try_new(&mut body, schema) + .unwrap() + .finish() + .unwrap(); + body + } + + fn assert_fetch_blobs_http_error(error: Error, expected: &str) { + match error { + Error::Http { + source, request_id, .. + } => { + assert!(source.to_string().contains(expected)); + assert!(!request_id.is_empty()); + } + error => panic!("expected HTTP error, got {error}"), + } + } + + fn assert_not_supported_error(error: Error, expected: &str) { + match error { + Error::NotSupported { message } => assert!(message.contains(expected)), + error => panic!("expected not-supported error, got {error}"), + } + } + + #[tokio::test] + async fn test_fetch_blobs_rejects_missing_column() { + let batch = RecordBatch::try_new( + Arc::new(Schema::new(vec![Field::new( + "other", + DataType::LargeBinary, + true, + )])), + vec![Arc::new(LargeBinaryArray::from(vec![Some( + b"value".as_slice(), + )]))], + ) + .unwrap(); + let table = table_with_fetch_blobs_response(write_ipc_stream_uncompressed(&batch)); + + let error = table.fetch_blobs("image", &[10]).await.unwrap_err(); + assert_fetch_blobs_http_error(error, "missing the 'image' column"); + } + + #[tokio::test] + async fn test_fetch_blobs_rejects_wrong_column_type() { + let batch = RecordBatch::try_new( + Arc::new(Schema::new(vec![Field::new( + "image", + DataType::Int32, + false, + )])), + vec![Arc::new(Int32Array::from(vec![1]))], + ) + .unwrap(); + let table = table_with_fetch_blobs_response(write_ipc_stream_uncompressed(&batch)); + + let error = table.fetch_blobs("image", &[10]).await.unwrap_err(); + assert_fetch_blobs_http_error( + error, + "type Int32, expected Binary, LargeBinary, or BinaryView", + ); + } + + #[rstest] + #[case(DataType::Binary)] + #[case(DataType::LargeBinary)] + #[case(DataType::BinaryView)] + #[tokio::test] + async fn test_fetch_blobs_accepts_binary_large_binary_and_binary_view( + #[case] data_type: DataType, + ) { + let binary_values = BinaryArray::from(vec![ + Some(b"alpha".as_slice()), + None, + Some(b"gamma".as_slice()), + ]); + let typed_column = arrow::compute::cast(&binary_values, &data_type).unwrap(); + let batch = RecordBatch::try_new( + Arc::new(Schema::new(vec![Field::new("image", data_type, true)])), + vec![typed_column], + ) + .unwrap(); + let table = table_with_fetch_blobs_response(write_ipc_stream_uncompressed(&batch)); + + let blobs = table.fetch_blobs("image", &[10, 20, 30]).await.unwrap(); + assert_eq!(blobs.len(), 3); + assert_eq!(blobs.value(0), b"alpha"); + assert!(blobs.is_null(1)); + assert_eq!(blobs.value(2), b"gamma"); + } + + #[tokio::test] + async fn test_fetch_blobs_rejects_row_count_mismatch() { + let batch = RecordBatch::try_new( + Arc::new(Schema::new(vec![Field::new( + "image", + DataType::LargeBinary, + true, + )])), + vec![Arc::new(LargeBinaryArray::from(vec![Some( + b"value".as_slice(), + )]))], + ) + .unwrap(); + let table = table_with_fetch_blobs_response(write_ipc_stream_uncompressed(&batch)); + + let error = table.fetch_blobs("image", &[10, 20]).await.unwrap_err(); + assert_fetch_blobs_http_error(error, "returned 1 rows for 2 row ids"); + } + + #[tokio::test] + async fn test_fetch_blobs_rejects_zero_batches_for_nonempty_row_ids() { + let schema = Schema::new(vec![Field::new("image", DataType::LargeBinary, true)]); + let table = table_with_fetch_blobs_response(write_empty_ipc_stream(&schema)); + + let error = table.fetch_blobs("image", &[10]).await.unwrap_err(); + assert_fetch_blobs_http_error(error, "returned 0 rows for 1 row ids"); + } + + #[tokio::test] + async fn test_fetch_blobs_skips_the_request_for_empty_row_ids() { + let table = Table::new_with_handler("my_table", |_| -> http::Response { + panic!("fetch_blobs must not call the server for an empty selection"); + }); + + let blobs = table.fetch_blobs("image", &[]).await.unwrap(); + assert!(blobs.is_empty()); + } + + #[tokio::test] + async fn test_blob_byte_apis_not_supported_on_old_server() { + let table = Table::new_with_handler("my_table", |_| -> http::Response { + panic!("blob request must not reach a server without blob support"); + }); + + assert_not_supported_error( + table.fetch_blobs("image", &[1]).await.unwrap_err(), + "fetch_blobs", + ); + + let message = table + .fetch_blob_files("image", &[1]) + .await + .unwrap_err() + .to_string(); + assert!( + message.contains("fetch_blob_files is not supported on LanceDB Cloud"), + "got: {message}" + ); + assert!( + !message.contains("Use fetch_blobs"), + "old server must not be told to use fetch_blobs, got: {message}" + ); + } + + #[tokio::test] + async fn test_blob_files_point_at_fetch_blobs_on_a_blob_capable_server() { + let table = Table::new_with_handler_version( + "my_table", + semver::Version::new(0, 5, 0), + |_| -> http::Response { panic!("fetch_blob_files must not reach the server") }, + ); + + assert_not_supported_error( + table.fetch_blob_files("image", &[1]).await.unwrap_err(), + "Use fetch_blobs for full bytes", + ); + } + + #[rstest] + #[case(ARROW_STREAM_CONTENT_TYPE)] + #[case("application/vnd.apache.arrow.stream; charset=utf-8")] + #[case("APPLICATION/VND.APACHE.ARROW.STREAM")] + #[tokio::test] + async fn test_fetch_blobs_accepts_stream_content_type_variants( + #[case] content_type: &'static str, + ) { + let table = table_with_fetch_blobs_content_type( + Some(content_type), + write_ipc_stream_uncompressed(&one_row_blob_batch("image")), + ); + + let blobs = table.fetch_blobs("image", &[10]).await.unwrap(); + + assert_eq!(blobs.value(0), b"alpha"); + } + + // Shared decoder accepts file framing because /query still returns it. + // fetch_blobs wire contract is stream. Enforced on the server. + #[rstest] + #[case(ARROW_FILE_CONTENT_TYPE)] + #[case("application/vnd.apache.arrow.file; charset=utf-8")] + #[case("APPLICATION/VND.APACHE.ARROW.FILE")] + #[tokio::test] + async fn test_fetch_blobs_accepts_file_content_type_variants( + #[case] content_type: &'static str, + ) { + let table = table_with_fetch_blobs_content_type( + Some(content_type), + write_ipc_file(&one_row_blob_batch("image")), + ); + + let blobs = table.fetch_blobs("image", &[10]).await.unwrap(); + + assert_eq!(blobs.value(0), b"alpha"); + } + + #[rstest] + #[case(ARROW_STREAM_CONTENT_TYPE, write_ipc_file(&one_row_blob_batch("image")))] + #[case( + ARROW_FILE_CONTENT_TYPE, + write_ipc_stream_uncompressed(&one_row_blob_batch("image")) + )] + #[tokio::test] + async fn test_fetch_blobs_fails_when_the_body_contradicts_the_content_type( + #[case] content_type: &'static str, + #[case] body: Vec, + ) { + let table = table_with_fetch_blobs_content_type(Some(content_type), body); + + let error = table.fetch_blobs("image", &[10]).await.unwrap_err(); + + assert!( + matches!(error, Error::Arrow { .. }), + "expected an Arrow decode failure, got {error}" + ); + } + + #[tokio::test] + async fn test_fetch_blobs_rejects_a_response_that_is_not_arrow_ipc() { + let table = table_with_fetch_blobs_content_type( + Some("application/json"), + br#"{"blobs": []}"#.to_vec(), + ); + + let error = table.fetch_blobs("image", &[10]).await.unwrap_err(); + assert_fetch_blobs_http_error(error, "got 'application/json'"); + } + + #[tokio::test] + async fn test_fetch_blobs_without_content_type_falls_back_to_file_framing() { + let table = + table_with_fetch_blobs_content_type(None, write_ipc_file(&one_row_blob_batch("image"))); + + let blobs = table.fetch_blobs("image", &[10]).await.unwrap(); + + assert_eq!(blobs.value(0), b"alpha"); + } + #[tokio::test] async fn test_query_vector_default_values() { let expected_data = RecordBatch::try_new( diff --git a/rust/lancedb/src/remote/table/blobs.rs b/rust/lancedb/src/remote/table/blobs.rs new file mode 100644 index 000000000..550f72a4b --- /dev/null +++ b/rust/lancedb/src/remote/table/blobs.rs @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The LanceDB Authors + +//! Cloud blob column listing and whole-byte fetch. + +use std::sync::Arc; + +use arrow_array::{Array, LargeBinaryArray}; +use arrow_schema::DataType; +use futures::TryStreamExt; + +use crate::Error; +use crate::blob::BlobFile; +use crate::error::Result; +use crate::remote::client::HttpSend; +use crate::table::BaseTable; + +use super::RemoteTable; + +impl RemoteTable { + /// Blob v2 columns are marked in field metadata, which `describe` returns. Reading + /// them from the cached schema needs no route of its own and no version gate. + pub(super) async fn blob_columns_impl(&self) -> Result> { + let schema = self.schema().await?; + Ok(crate::blob::blob_column_names(schema.as_ref())) + } + + pub(super) async fn fetch_blobs_impl( + &self, + column: &str, + row_ids: &[u64], + ) -> Result { + // An empty selection already has its answer, so skip the round trip and the + // server requirement entirely. Local fetch_blobs returns early the same way. + if row_ids.is_empty() { + return Ok(LargeBinaryArray::from(Vec::>::new())); + } + if !self.server_version.support_blobs() { + return Err(Error::NotSupported { + message: "fetch_blobs is not supported on this LanceDB Cloud server".into(), + }); + } + let version = self.current_version().await; + let mut body = serde_json::json!({ + "version": version, + "column": column, + "row_ids": row_ids, + }); + self.apply_branch_body(&mut body); + + let request = self + .post_read(&format!("/v1/table/{}/fetch_blobs/", self.identifier)) + .json(&body); + let (request_id, response) = self.send(request, true).await?; + let mut stream = self.read_arrow_response(&request_id, response).await?; + + let mut blob_chunks: Vec> = Vec::new(); + while let Some(batch) = stream.try_next().await? { + let blob_column = batch.column_by_name(column).ok_or_else(|| Error::Http { + source: format!("fetch_blobs response is missing the '{column}' column").into(), + request_id: request_id.clone(), + status_code: None, + })?; + // The server returns LargeBinary today. Accept the other binary types so a + // server that switches encodings does not break older clients. + if !matches!( + blob_column.data_type(), + DataType::Binary | DataType::LargeBinary | DataType::BinaryView + ) { + return Err(Error::Http { + source: format!( + "fetch_blobs response column has type {}, expected Binary, LargeBinary, or BinaryView", + blob_column.data_type() + ) + .into(), + request_id: request_id.clone(), + status_code: None, + }); + } + blob_chunks.push(arrow::compute::cast(blob_column, &DataType::LargeBinary)?); + } + let blobs = if blob_chunks.is_empty() { + LargeBinaryArray::from(Vec::>::new()) + } else { + let blob_chunk_refs: Vec<&dyn Array> = blob_chunks.iter().map(AsRef::as_ref).collect(); + arrow::compute::concat(&blob_chunk_refs)? + .as_any() + .downcast_ref::() + .ok_or_else(|| Error::Http { + source: "fetch_blobs could not read the concatenated response as LargeBinary" + .into(), + request_id: request_id.clone(), + status_code: None, + })? + .clone() + }; + if blobs.len() != row_ids.len() { + return Err(Error::Http { + source: format!( + "fetch_blobs returned {} rows for {} row ids", + blobs.len(), + row_ids.len() + ) + .into(), + request_id, + status_code: None, + }); + } + Ok(blobs) + } + + pub(super) async fn fetch_blob_files_impl( + &self, + _column: &str, + _row_ids: &[u64], + ) -> Result>> { + let message = if self.server_version.support_blobs() { + "fetch_blob_files is not supported on LanceDB Cloud yet. \ + Use fetch_blobs for full bytes" + } else { + "fetch_blob_files is not supported on LanceDB Cloud" + }; + Err(Error::NotSupported { + message: message.into(), + }) + } +} From 48945d0658c21f9672077e89e26fde55a972dbdb Mon Sep 17 00:00:00 2001 From: "Farmer.Chillax" <48387781+FarmerChillax@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:20:47 +0800 Subject: [PATCH 20/29] feat(python): add namespace/table exist support (#3460) In the current LanceDB usage implementation, there is no way to check whether a table or namespace already exists. This PR introduces the namespace_exists and table_exists methods to determine the existence of tables and namespaces. useage like this: ``` # check table exists db.table_exists(table_id=['xxx']) # check namespace exists db.namespace_exists(namespace_id=['xxx']) ``` fixes: #3419 --------- Signed-off-by: farmer Co-authored-by: Claude Opus 4.7 --- python/python/lancedb/db.py | 45 + python/python/lancedb/namespace.py | 96 +- .../tests/test_namespace_integration.py | 70 +- python/uv.lock | 2234 +++++++++-------- 4 files changed, 1377 insertions(+), 1068 deletions(-) diff --git a/python/python/lancedb/db.py b/python/python/lancedb/db.py index fd6240d48..75ff7239d 100644 --- a/python/python/lancedb/db.py +++ b/python/python/lancedb/db.py @@ -178,6 +178,51 @@ class DBConnection(EnforceOverrides): "Namespace operations are not supported for this connection type" ) + def namespace_exists(self, namespace_id: List[str]) -> bool: + """Check if a namespace exists. + + Parameters + ---------- + namespace_id: List[str] + The namespace identifier to check. + + Returns + ------- + bool + True if the namespace exists, False otherwise. + + Raises + ------ + NotImplementedError + If the connection type does not support namespace operations. + """ + raise NotImplementedError( + "Namespace operations are not supported for this connection type" + ) + + def table_exists(self, table_id: List[str]) -> bool: + """Check if a table exists. + + Parameters + ---------- + table_id: List[str] + The table identifier to check (full path including namespace + segments and table name). + + Returns + ------- + bool + True if the table exists, False otherwise. + + Raises + ------ + NotImplementedError + If the connection type does not support namespace operations. + """ + raise NotImplementedError( + "Namespace operations are not supported for this connection type" + ) + def list_tables( self, namespace_path: Optional[List[str]] = None, diff --git a/python/python/lancedb/namespace.py b/python/python/lancedb/namespace.py index 2759f6e43..b151395cc 100644 --- a/python/python/lancedb/namespace.py +++ b/python/python/lancedb/namespace.py @@ -38,7 +38,11 @@ from lance_namespace_urllib3_client.models.query_table_request_vector import ( QueryTableRequestVector, ) from lance_namespace_urllib3_client.models.string_fts_query import StringFtsQuery -from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError +from lance_namespace.errors import ( + NamespaceNotEmptyError, + NamespaceNotFoundError, + TableNotFoundError, +) from lancedb._lancedb import ( connect_namespace as _connect_namespace, connect_namespace_client as _connect_namespace_client, @@ -53,6 +57,8 @@ from lance_namespace import ( DropNamespaceResponse, ListNamespacesResponse, ListTablesResponse, + NamespaceExistsRequest, + TableExistsRequest, ) from lancedb.table import AsyncTable, LanceTable, Table from lancedb.util import validate_table_name @@ -780,6 +786,51 @@ class LanceNamespaceDBConnection(DBConnection): """ return LOOP.run(self._inner.describe_namespace(namespace_path)) + @override + def namespace_exists(self, namespace_id: List[str]) -> bool: + """ + Check if a namespace exists. + + Parameters + ---------- + namespace_id : List[str] + The namespace identifier to check. + + Returns + ------- + bool + True if the namespace exists, False otherwise. + """ + request = NamespaceExistsRequest(id=namespace_id) + try: + self._namespace_client.namespace_exists(request) + return True + except NamespaceNotFoundError: + return False + + @override + def table_exists(self, table_id: List[str]) -> bool: + """ + Check if a table exists. + + Parameters + ---------- + table_id : List[str] + The table identifier to check (full path including namespace + segments and table name). + + Returns + ------- + bool + True if the table exists, False otherwise. + """ + request = TableExistsRequest(id=table_id) + try: + self._namespace_client.table_exists(request) + return True + except TableNotFoundError: + return False + @override def list_tables( self, @@ -1233,6 +1284,49 @@ class AsyncLanceNamespaceDBConnection: """ return await self._inner.describe_namespace(namespace_path) + async def namespace_exists(self, namespace_id: List[str]) -> bool: + """ + Check if a namespace exists. + + Parameters + ---------- + namespace_id : List[str] + The namespace identifier to check. + + Returns + ------- + bool + True if the namespace exists, False otherwise. + """ + request = NamespaceExistsRequest(id=namespace_id) + try: + self._namespace_client.namespace_exists(request) + return True + except NamespaceNotFoundError: + return False + + async def table_exists(self, table_id: List[str]) -> bool: + """ + Check if a table exists. + + Parameters + ---------- + table_id : List[str] + The table identifier to check (full path including namespace + segments and table name). + + Returns + ------- + bool + True if the table exists, False otherwise. + """ + request = TableExistsRequest(id=table_id) + try: + self._namespace_client.table_exists(request) + return True + except TableNotFoundError: + return False + async def list_tables( self, namespace_path: Optional[List[str]] = None, diff --git a/python/python/tests/test_namespace_integration.py b/python/python/tests/test_namespace_integration.py index 84455c979..e05885224 100644 --- a/python/python/tests/test_namespace_integration.py +++ b/python/python/tests/test_namespace_integration.py @@ -18,6 +18,7 @@ Tests verify: """ import copy +import os import shutil import sys import tempfile @@ -239,7 +240,7 @@ def create_tracking_namespace( dir_props = {f"storage.{k}": v for k, v in storage_options_with_refresh.items()} - if bucket_name.startswith("/") or bucket_name.startswith("file://"): + if os.path.isabs(bucket_name) or bucket_name.startswith("file://"): dir_props["root"] = f"{bucket_name}/namespace_root" else: dir_props["root"] = f"s3://{bucket_name}/namespace_root" @@ -767,3 +768,70 @@ def test_namespace_with_schema_only(s3_bucket: str, use_custom: bool): # Verify data was added assert table.count_rows() == 2 + + +@pytest.mark.parametrize("use_custom", [False, True], ids=["DirectoryNS", "CustomNS"]) +def test_namespace_exists(use_custom: bool): + """ + Test namespace_exists returns True for existing and False for non-existent. + """ + temp_dir = tempfile.mkdtemp() + try: + ns_client, _ = create_tracking_namespace( + bucket_name=temp_dir, + storage_options={}, + credential_expires_in_seconds=3600, + use_custom=use_custom, + ) + db = LanceNamespaceDBConnection(ns_client) + + namespace_name = f"test_ns_{uuid.uuid4().hex[:8]}" + db.create_namespace([namespace_name]) + + # Existing namespace should return True + assert db.namespace_exists(namespace_id=[namespace_name]) is True + + # Non-existent namespace should return False + assert db.namespace_exists(namespace_id=["nonexistent_ns"]) is False + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + +@pytest.mark.parametrize("use_custom", [False, True], ids=["DirectoryNS", "CustomNS"]) +def test_table_exists(use_custom: bool): + """ + Test table_exists returns True for existing table and False for non-existent. + """ + temp_dir = tempfile.mkdtemp() + try: + ns_client, _ = create_tracking_namespace( + bucket_name=temp_dir, + storage_options={}, + credential_expires_in_seconds=3600, + use_custom=use_custom, + ) + db = LanceNamespaceDBConnection(ns_client) + + namespace_name = f"test_ns_{uuid.uuid4().hex[:8]}" + db.create_namespace([namespace_name]) + + table_name = f"test_table_{uuid.uuid4().hex}" + namespace_path = [namespace_name] + schema = pa.schema( + [ + pa.field("id", pa.int64()), + pa.field("vector", pa.list_(pa.float32(), 2)), + pa.field("text", pa.string()), + ] + ) + + db.create_table(table_name, schema=schema, namespace_path=namespace_path) + + # Existing table should return True + table_id = namespace_path + [table_name] + assert db.table_exists(table_id=table_id) is True + + # Non-existent table should return False + assert db.table_exists(table_id=namespace_path + ["nonexistent_table"]) is False + finally: + shutil.rmtree(temp_dir, ignore_errors=True) diff --git a/python/uv.lock b/python/uv.lock index fc1260854..551dc3f68 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -12,21 +12,22 @@ resolution-markers = [ [[package]] name = "accelerate" -version = "1.13.0" +version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, { name = "safetensors" }, { name = "torch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/14/787e5498cd062640f0f3d92ef4ae4063174f76f9afd29d13fc52a319daae/accelerate-1.13.0.tar.gz", hash = "sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236", size = 402835, upload-time = "2026-03-04T19:34:12.359Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/75/94cd5d389649578aca399e5aa822637eec18319a1dadc400ffe2f9a7493f/accelerate-1.14.0.tar.gz", hash = "sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d", size = 412167, upload-time = "2026-06-11T13:45:52.326Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl", hash = "sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0", size = 383744, upload-time = "2026-03-04T19:34:10.313Z" }, + { url = "https://files.pythonhosted.org/packages/a8/db/253133d7e7cb40d3af384bb2f5c0b4a2b7fdcffbc95c688cc67a20a3c103/accelerate-1.14.0-py3-none-any.whl", hash = "sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6", size = 389246, upload-time = "2026-06-11T13:45:50.477Z" }, ] [[package]] @@ -46,16 +47,16 @@ wheels = [ [[package]] name = "aiohappyeyeballs" -version = "2.6.2" +version = "2.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, ] [[package]] name = "aiohttp" -version = "3.14.0" +version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -68,126 +69,126 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/f0/f81190ba488cd106c2fc6d92680e56bb223bbbbf1e6908c2617011290112/aiohttp-3.14.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:692e409052e7436029bbb32977cd7c5bf806ac5fa4085b973996785ffadad33c", size = 760606, upload-time = "2026-06-01T19:36:39.054Z" }, - { url = "https://files.pythonhosted.org/packages/f6/54/444d37eebf0f15db661ca44ec7caf93962f3c5ca92eb4c9a5d888b70aaa2/aiohttp-3.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40af7ebe53c7990e110dc4ad03566b12c3ac996254298a3d39046dd69cfcb2c2", size = 514677, upload-time = "2026-06-01T19:36:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d1/da280e23321c132c0a3fa7c8cc2830621d79174edc64c829443346489a36/aiohttp-3.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb2ffbb7da32f82e21ad9952669c45bd88a80e0878264c2f59fe1c6fb2badd", size = 510155, upload-time = "2026-06-01T19:36:44.072Z" }, - { url = "https://files.pythonhosted.org/packages/09/b8/2e36d54d0991ec5bba451444004591ee0af58cb1662a3a81c562878b9c1f/aiohttp-3.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2514cb7195f6d7c219339635bea71ae47d1569b051300d32df9dcfabcdb869", size = 1699947, upload-time = "2026-06-01T19:36:45.762Z" }, - { url = "https://files.pythonhosted.org/packages/57/95/a31d8ea1a0b9ecc084f5a7dd0b431ce64ef585918bb7bdc82afe11843877/aiohttp-3.14.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:30e8b7eeb42d02c120ca90d6c6e076a221a16b70a6dac9ae44c7ab5104cc7fe4", size = 1664364, upload-time = "2026-06-01T19:36:47.653Z" }, - { url = "https://files.pythonhosted.org/packages/01/f6/5de3ddffc87a9e8d09b3be38fbd6dd1a736b2ad477a7e787dcb85f57f338/aiohttp-3.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63e38be0d75a654deaa06be32fb4cab883a4222940be1d05861b6717679cbadb", size = 1761186, upload-time = "2026-06-01T19:36:49.355Z" }, - { url = "https://files.pythonhosted.org/packages/33/8c/03c5438ec35d7e3a4f33fe895d6c3ec7540a7cec46065f21851211e1ee4d/aiohttp-3.14.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1210d4c87cc00128160c7384ab41877a701295b97cffa6362f908a49b6e8a7ca", size = 1849727, upload-time = "2026-06-01T19:36:51.478Z" }, - { url = "https://files.pythonhosted.org/packages/22/32/5a05303b0874458920b73f48b8779cc3a93d503f121b38dcc0456dbd698c/aiohttp-3.14.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a78a77366ed158a0a54b076990e575d7b7cdb728cbfd02711eadab150f2269f", size = 1708197, upload-time = "2026-06-01T19:36:53.241Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/478f169488d61414c0a05e7fe423b59ae3d9dcc933d1f0e4acc2c5d5bc3e/aiohttp-3.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f4d2038c64f36df96cfd3fa0937910e231eafbf897e70a06c155a817bb632fa6", size = 1578147, upload-time = "2026-06-01T19:36:55.154Z" }, - { url = "https://files.pythonhosted.org/packages/1d/af/b20af85765658972d3337834bd5eebba91b962794f2b4fc3e0ee8c85c0e1/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4714c70067a08b604d0bf3bc4dfdf82e52944afab41d0428d460862763d2f79b", size = 1665836, upload-time = "2026-06-01T19:36:56.94Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a3/771879cfd59948f4544b172189048905feff802f20f1c6c5411e998a3e06/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f79bfd2847513a7ac801bbafd1de02348a37926ac439eeb4bfe96fcff4eada15", size = 1680335, upload-time = "2026-06-01T19:36:58.642Z" }, - { url = "https://files.pythonhosted.org/packages/f4/16/582e36ad1d32133cd40659f3bc98e71c22179665a1cfbbb4713bce339c06/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:25e9f1d2465a210d60edb64d7b204a147e85d4c194eecef3d1604fb5ace678ce", size = 1731180, upload-time = "2026-06-01T19:37:00.583Z" }, - { url = "https://files.pythonhosted.org/packages/11/bc/80708fe3f64a07a2c306a42fc7b009118a952709761d215f6d1b4c57195b/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b5314743ebe926c2fda35d0a298c565c885505f6635c2a30936363404cf274a7", size = 1565805, upload-time = "2026-06-01T19:37:02.446Z" }, - { url = "https://files.pythonhosted.org/packages/57/8f/8d25897f8273a32fe4ad40a8885eec4f397377ed46e8e383078169f60316/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:28eee8de1d69711c53116df8202f1c2aa0e3f80ef912a88fc18d159d53e7110b", size = 1742496, upload-time = "2026-06-01T19:37:04.222Z" }, - { url = "https://files.pythonhosted.org/packages/9f/7d/c341d32ab2dec56c8478740695743dc6c21b383cace9376a3eab16311a07/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89ed35666c95d3efe1955056afcde09e62a57a34e2a4398b17f9f6c1564f0b25", size = 1691240, upload-time = "2026-06-01T19:37:06.277Z" }, - { url = "https://files.pythonhosted.org/packages/37/0f/a81207dd7a2d4a4f645b3a3f8b5a1da1159dc63117ffb137b698fd6df50f/aiohttp-3.14.0-cp310-cp310-win32.whl", hash = "sha256:5e4646e9a6af29af354204011bf5769cb0276ec5b64653e42f90b3e13845169f", size = 454686, upload-time = "2026-06-01T19:37:07.96Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/842357f2afb9c915715c6f5775239d987f5d0f845abf7675fa794e0a9d40/aiohttp-3.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:22a8d06f204e0518a586d770032db3c7043c9ba3693081b3e3ad425e1458d594", size = 478677, upload-time = "2026-06-01T19:37:09.652Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d1/330fb22c9535ec177b52396905131c6e39447244b6ca876262939af668ef/aiohttp-3.14.0-cp310-cp310-win_arm64.whl", hash = "sha256:4acfc34bd4d3c58754fc9f22ff1b5e92aabce68f3d4bf7b71a0b732d9bceb78a", size = 450364, upload-time = "2026-06-01T19:37:11.279Z" }, - { url = "https://files.pythonhosted.org/packages/67/47/7727bfe8db93f8835a001bd4359d8480cc68d1259b8bce334668f8be97bd/aiohttp-3.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a", size = 759147, upload-time = "2026-06-01T19:37:12.918Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f2/cd3fedff6fade73d71df9ec908c210cec518ef90fd00289250684b90aecf/aiohttp-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803", size = 513705, upload-time = "2026-06-01T19:37:14.633Z" }, - { url = "https://files.pythonhosted.org/packages/5a/fe/49746b6b610144a06323bebd8e1211a390310d8c69b98dd6d52df341bc3e/aiohttp-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e", size = 509627, upload-time = "2026-06-01T19:37:16.385Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3f/28f2f6cf3d5c0e7b01b27140d0e7873fd11fb341169ad3ce78ad04aba628/aiohttp-3.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903", size = 1769293, upload-time = "2026-06-01T19:37:18.067Z" }, - { url = "https://files.pythonhosted.org/packages/97/6f/2e5f1b525d5474b12b3c60abf733a755845f3bceff21542081ada515f837/aiohttp-3.14.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb", size = 1732363, upload-time = "2026-06-01T19:37:20.138Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ce/596120faa85ca7b19cd061e3f2f3be23aa8f11a0aedf9191db9e0da1bd76/aiohttp-3.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2", size = 1840375, upload-time = "2026-06-01T19:37:22.104Z" }, - { url = "https://files.pythonhosted.org/packages/72/3c/a7ffe05a757a4a7867643da69357ec41f506879fbd1b231d2ed90af246b2/aiohttp-3.14.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81", size = 1921484, upload-time = "2026-06-01T19:37:24.068Z" }, - { url = "https://files.pythonhosted.org/packages/93/fa/2c861170bbd4a491de93a69e081db1d971092569e0d593a98ef62c384dc1/aiohttp-3.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee", size = 1774153, upload-time = "2026-06-01T19:37:26.256Z" }, - { url = "https://files.pythonhosted.org/packages/9d/da/1d2f5a165f47ec9b1f69d37b8b977fdc4d501aa72ffb7930db27bb9e49ea/aiohttp-3.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d", size = 1632569, upload-time = "2026-06-01T19:37:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/46/1d/7a6e295c4257252f70f69e90864fdad74b6a1293054fb3f9e65a15de6d63/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00", size = 1740325, upload-time = "2026-06-01T19:37:30.08Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7e/e1899b1ca3ec62f1eab2a5cbde14039b97493f7f53eb88d9b668562ffa8d/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026", size = 1748691, upload-time = "2026-06-01T19:37:32.211Z" }, - { url = "https://files.pythonhosted.org/packages/ec/54/4e6b61c1fe7d3433f82bcc6bd7e4d7c683a742a10c9b12a025fd3695c047/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86", size = 1814477, upload-time = "2026-06-01T19:37:34.173Z" }, - { url = "https://files.pythonhosted.org/packages/9c/38/86fd51be2e08d8e45c83d879d255f10391903cd9fe2a16512f7591a15873/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f", size = 1623393, upload-time = "2026-06-01T19:37:36.281Z" }, - { url = "https://files.pythonhosted.org/packages/78/49/466e947a42a88ee23c486d036e7e5d1b097f1bafd8084ad9c9a0a92f0f43/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93", size = 1824097, upload-time = "2026-06-01T19:37:38.421Z" }, - { url = "https://files.pythonhosted.org/packages/f3/89/35f3410bc284682338a1be6b6ea0c5abfa05f063942cfaa9256608440434/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996", size = 1764790, upload-time = "2026-06-01T19:37:40.755Z" }, - { url = "https://files.pythonhosted.org/packages/42/80/2d4291bd5724d3d17e5951aff5a3e02281483fb47295f0788276ee66cd73/aiohttp-3.14.0-cp311-cp311-win32.whl", hash = "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae", size = 454176, upload-time = "2026-06-01T19:37:42.837Z" }, - { url = "https://files.pythonhosted.org/packages/59/ed/41d0ad4f6ececffc32bdf1f7b494e5498f7ca5c849ea2e3cc9bbd1668251/aiohttp-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5", size = 479334, upload-time = "2026-06-01T19:37:44.776Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/c0b5e305c770053f8c3d069bb52b8196917ba91949d1962d52eb307fb0d2/aiohttp-3.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43", size = 450262, upload-time = "2026-06-01T19:37:46.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/97/2b6889bfb6b6847520d50d95eb8c4307a45e28aaca39faf4a9454b3d1b2f/aiohttp-3.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e", size = 750194, upload-time = "2026-06-01T19:37:48.164Z" }, - { url = "https://files.pythonhosted.org/packages/21/e2/62634b7fff918ed98c3c6b2f0e70d520f7f28846cb412d451b04354c6459/aiohttp-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c", size = 506966, upload-time = "2026-06-01T19:37:50.014Z" }, - { url = "https://files.pythonhosted.org/packages/dd/fb/5ce075150828c797a5106f1c2fb26034e709d4289b9d2bf8b07f1e59fac6/aiohttp-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff", size = 507527, upload-time = "2026-06-01T19:37:51.96Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/405a0ae4e6b081754a3609c1c97c63a950e000a2def16046f1e736933a0e/aiohttp-3.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108", size = 1762420, upload-time = "2026-06-01T19:37:53.839Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1d/e05a7c896b15a6bc6fb8fc5319eb437861c2c49c34559ef928add6590315/aiohttp-3.14.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a", size = 1733672, upload-time = "2026-06-01T19:37:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/cc/22/a72f7c459e195fa41bf4f7abd1f925b91fe91f8097e51c654229ba144a33/aiohttp-3.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500", size = 1805064, upload-time = "2026-06-01T19:37:57.931Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/e85bdaba0be59ca4838005ebfef4048fcdd5f35a02b07057a9a123394440/aiohttp-3.14.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955", size = 1902125, upload-time = "2026-06-01T19:38:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/19/d8/51de5c6b971c27bb1ef620293b8d1ca611ec78736b34b3f6ccf68e4c8785/aiohttp-3.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2", size = 1783112, upload-time = "2026-06-01T19:38:02.641Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b4402bfde77e43dfb1b6ccff83c7b7ab63ed06b50c4754f0c5423fb374fe/aiohttp-3.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159", size = 1586356, upload-time = "2026-06-01T19:38:04.637Z" }, - { url = "https://files.pythonhosted.org/packages/bc/05/750a3265ca4dc54a460bd0cb1121a8f2ce9171fce4a135fb47ea7fd594d2/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02", size = 1723119, upload-time = "2026-06-01T19:38:06.713Z" }, - { url = "https://files.pythonhosted.org/packages/37/01/8c0812c50b3b1b1c37b323bf170d6be8847a8f234060485b7d1e71953f60/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd", size = 1757216, upload-time = "2026-06-01T19:38:08.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/2a/50fb98028a26887cbe48dcc1df92a90825615bc73b5584301304090cded8/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef", size = 1770500, upload-time = "2026-06-01T19:38:11.111Z" }, - { url = "https://files.pythonhosted.org/packages/bd/32/0ffd598a2fa2b9a423daf242e700cfdabda35d6e602394ad9ae58972c1c7/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e", size = 1576224, upload-time = "2026-06-01T19:38:13.391Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f9/b9fc381dd9b66afb33f2634c40e229d106467be0afcabe79648631ab6712/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae", size = 1794252, upload-time = "2026-06-01T19:38:15.498Z" }, - { url = "https://files.pythonhosted.org/packages/a8/fb/05d9214c975f23225a8cd5c439325e338c7c377b315480ef3871db51f54e/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066", size = 1760193, upload-time = "2026-06-01T19:38:17.624Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4b/02992fc4fb9e1b6673ee3f888a8e587a6447afda1f6f4aca776c148c2876/aiohttp-3.14.0-cp312-cp312-win32.whl", hash = "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430", size = 448650, upload-time = "2026-06-01T19:38:19.545Z" }, - { url = "https://files.pythonhosted.org/packages/39/e9/246532214c3abda518477cbaaf16d420295ad8effa5233844cbb38f299ab/aiohttp-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a", size = 476145, upload-time = "2026-06-01T19:38:21.505Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c3/63f8c20090048915711598b0adf475b149216d736157961de06480a45b15/aiohttp-3.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370", size = 444250, upload-time = "2026-06-01T19:38:24.027Z" }, - { url = "https://files.pythonhosted.org/packages/21/61/d11f7d9a3144bffe825247d6367cd93053666da50b94707c9129c78868d5/aiohttp-3.14.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:25400d710641a8040bf022a8a99f579e581ffa1c5bd42c33255d7d6f3957c127", size = 502399, upload-time = "2026-06-01T19:38:25.955Z" }, - { url = "https://files.pythonhosted.org/packages/4f/9b/a7e317625d36356844f8bb022cabd305b541f968856cc3c2e0b58e53ee6e/aiohttp-3.14.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c5492b9929826e07cc3fcb9739ae87aab05dff6b5e67a9b73fd1700c6d008981", size = 510068, upload-time = "2026-06-01T19:38:27.828Z" }, - { url = "https://files.pythonhosted.org/packages/11/41/cc2d2cfbfbdc3126ba258f3cd27d1ac8a33492ae3c35a4583ee21f0ba7f1/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3366751d68d237c621264233a32f3078bbc21b7904ab90a77e03d21390c742c6", size = 481670, upload-time = "2026-06-01T19:38:29.836Z" }, - { url = "https://files.pythonhosted.org/packages/3c/07/381f4023c3b08cb616e520f566d8c58957abad54e56441d41fe67cfb0195/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:57ea07d28695a7a40304d42251892a8df765e5588c10ee32afeddcd5df33c0a2", size = 487591, upload-time = "2026-06-01T19:38:31.704Z" }, - { url = "https://files.pythonhosted.org/packages/fb/4d/4506fdb7a022bdf70011a3bbb4ca00c5c570026ef6a3c5bd7bc70c39089c/aiohttp-3.14.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:076cb014191ae2e65d949e1ad01f1dcfe33e32789b5172510f3e79c79fc04d50", size = 496503, upload-time = "2026-06-01T19:38:33.6Z" }, - { url = "https://files.pythonhosted.org/packages/ef/7d/c814111e04894a45d9e2defc94443879a6f118d9633d5fedfe6e2e8af5f0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f3fc37054564dee64a855b5b092d87ec35dcddfaabf7dacb1c8a2b1f83dc0a9", size = 745870, upload-time = "2026-06-01T19:38:36.013Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ee/80eee0efddfe187e7cd05027086b7ce1c0e492e82a4eda58f5c5543a44a0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8fcaef74d2ab0f607d7ff85a0d15e21bb5a258c4a58df1908396eb50d7f4ed3c", size = 505588, upload-time = "2026-06-01T19:38:38.282Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f8/0f28f04eef75d52fc9c715dde7ce9c0abb810fd20cfeb0fea7afd2ab1e98/aiohttp-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4c01b0bfc6209590960e68eac083cd22d5d87c21f974dd6208cafa5d3542bc8", size = 504492, upload-time = "2026-06-01T19:38:40.611Z" }, - { url = "https://files.pythonhosted.org/packages/ff/db/44c755232085545065c94378dfce38641b1aee647f4939fcd32f5b32e719/aiohttp-3.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f12eb7896e81caf403a2b18c9406426f1207361e7239c057ab29c076d4257e83", size = 1752111, upload-time = "2026-06-01T19:38:42.682Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6a/42e030a46743841414402a3b00cd3d78419055e86c66fb5822c14b5abfc6/aiohttp-3.14.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c79a044cacf360ec46738d863d2f41c9300d2a06ef4a7402ea0df306a350e61", size = 1729674, upload-time = "2026-06-01T19:38:44.79Z" }, - { url = "https://files.pythonhosted.org/packages/34/26/3199beb415202e3108e7b83ecebe10914d806d33fb9860c3e4aa60a19be3/aiohttp-3.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85e0675f47be4eff0636bf88c02140ea89168ae0df3ff1f3f464e9de9610d277", size = 1798808, upload-time = "2026-06-01T19:38:47.01Z" }, - { url = "https://files.pythonhosted.org/packages/bd/94/b9b6fcf0ee17c21d0d19fb8c22bf83ad18f82e702a9c3bd901a868f5e446/aiohttp-3.14.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b33e751cab03fdc960095b1e326cb5a03f5ee577d6ded59f3d1c100f8668882", size = 1891921, upload-time = "2026-06-01T19:38:49.233Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a3/3800dbd095cb2bb165a7ea5d94d790914677e27f45638c7d80e3f34c8945/aiohttp-3.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9224c6dd7f5c749aba4f61315a894601448b28d94d12f4dea0903e26d2096", size = 1777241, upload-time = "2026-06-01T19:38:52.04Z" }, - { url = "https://files.pythonhosted.org/packages/21/2a/45be91ad1b860508557448d4cc2e165a2ee68dd865657b73bf66cc5a00fb/aiohttp-3.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6281aecdf2732940f4fe06bd6adec5ae4d59b78b080b8e3a6b81467301010988", size = 1579554, upload-time = "2026-06-01T19:38:54.508Z" }, - { url = "https://files.pythonhosted.org/packages/b4/3d/dc94df99ed1511fdf28314f722643ed334112643cab00223577085e788c4/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23e8314e7aed8576fbe33314d218bd81447a3adbc91dc36f1163bf583cd3084c", size = 1714864, upload-time = "2026-06-01T19:38:56.788Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e4/1f1c8acbb3acd5c8f795473b92c9c3d44eb60a5692c6104256c8a1c83a0c/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3b54fbff46127aeafdd764cecd0d99fa2f24a0e37ea5c18a7c3a4ac450df1db3", size = 1749803, upload-time = "2026-06-01T19:38:59.367Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c8/c45ea6e7ed84cebba939b9c334498a045ba19d79c61b0110df5f21580de3/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b27d89af91a555f58e08e4902dbcbc48862fd40095720ca705990476bd93b7ac", size = 1765023, upload-time = "2026-06-01T19:39:01.651Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a1/a932941784432962fe390e1066823aaef64b4e5ac9fa595df57b5fe472a9/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:25d2326a4967bf705a9f9913a13005e93b6020ad8a9f6bd6bd78850d5171332e", size = 1571671, upload-time = "2026-06-01T19:39:04.044Z" }, - { url = "https://files.pythonhosted.org/packages/b0/01/e1280feac522597a4d46eb67a0cdfa053cfae263033030b761ab146f29fb/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a1d209375c503472b3c0a340cdf3c55fcd82e84b46dda7caeaced59faba373ec", size = 1789904, upload-time = "2026-06-01T19:39:06.294Z" }, - { url = "https://files.pythonhosted.org/packages/fa/10/ab28818262f4d26bdb47ed5f1fc7999b69e2fc6e0370b02d0f49011f45ea/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:666c7c5036df57b693026398b69b41874a1931ac5b3485fd910e57bfac253869", size = 1754516, upload-time = "2026-06-01T19:39:08.788Z" }, - { url = "https://files.pythonhosted.org/packages/af/cc/c122eabd7a1b7e0c9bbdd6be60e4715905b858399145d9df872bb94f1427/aiohttp-3.14.0-cp313-cp313-win32.whl", hash = "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456", size = 448656, upload-time = "2026-06-01T19:39:11.171Z" }, - { url = "https://files.pythonhosted.org/packages/41/a5/bab07d79848a00eedd8ed979ccb302aaea3ac6eb9fa16bd0ed87135869b4/aiohttp-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11", size = 475803, upload-time = "2026-06-01T19:39:13.439Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/f03ade8566c153666a3871afccbedf6d99911da006325e1fc6cf72a2de99/aiohttp-3.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b", size = 443889, upload-time = "2026-06-01T19:39:15.945Z" }, - { url = "https://files.pythonhosted.org/packages/28/03/5f36ab196a88ba5e9648ae5643e6531e67a3a8c0e96f9c6510ff41540fec/aiohttp-3.14.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:363ef9e91014e7891679bfb2ac0a7c6ea93435dbbfd10ecf41b9f06fcf506c5f", size = 503330, upload-time = "2026-06-01T19:39:18.195Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ce/8b49ec2f30f68e02f314f4832186cd45e583360a5a386058be36855d23b6/aiohttp-3.14.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:884a4edbdad77be9d0ef36142c8b504351b170df0bf62b51e784fadabf311c42", size = 509822, upload-time = "2026-06-01T19:39:20.396Z" }, - { url = "https://files.pythonhosted.org/packages/1a/fe/6edbf5d39bf29322b6816365b17ed8ede4dace164a3aea1abcd30110eb78/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:70ea956f6cc4a37620966b56c2e205d88ca3e6d85ec063277e414b1035cddad3", size = 483329, upload-time = "2026-06-01T19:39:22.607Z" }, - { url = "https://files.pythonhosted.org/packages/1b/5a/fae531bdbc6456fb6241f46b7b81e4d8a0dd3fc09118a0055dc7141ac1ec/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:ea3b9806c89f61da22fddf1f12dd524fb368e5e28f1261fbdafe5c3cd8ce893b", size = 489502, upload-time = "2026-06-01T19:39:24.881Z" }, - { url = "https://files.pythonhosted.org/packages/36/f4/48a7b0414db7fed77a03d5dde34508c026afd83510ab6bca08c313855776/aiohttp-3.14.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a071be341c2bd9b0188e62d173509f024e0a35b1c342c53c50f8daaeda8c3bd8", size = 497357, upload-time = "2026-06-01T19:39:27.197Z" }, - { url = "https://files.pythonhosted.org/packages/75/75/e85a13a370acc007fca5feb1fd1b88ac2d8426e6dadd625479b7cadd55a3/aiohttp-3.14.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:198cfe61bf253b19da1fb3e0fa122249dc4f14c12709493fed8054aa0411cc76", size = 750898, upload-time = "2026-06-01T19:39:29.563Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e4/3d637f800c724eff0e2bed64df72557444482366fd0a35b0cec0e6968f6c/aiohttp-3.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc203d6ce6b9106d54e2a93f41dfdfebfbca2d99962ba503bfd3e5921a6549e", size = 506986, upload-time = "2026-06-01T19:39:31.872Z" }, - { url = "https://files.pythonhosted.org/packages/1d/df/35161f3598bf7501d2b2a805b41ab4f45a2e34150c421bcb4ef8c0d281a7/aiohttp-3.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e19d17ab02bf16832a2c8c0d55a486792c5b1645665652ee9531aebcc30cb72", size = 508033, upload-time = "2026-06-01T19:39:34.137Z" }, - { url = "https://files.pythonhosted.org/packages/e5/39/b36e5d3d31e850fb4691dd3e941684ac490a2559249f6fa634b6b0fdf020/aiohttp-3.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d925fba0c14d5b498a8028b0107beebdfd16c5d48d702ff54f879cb017aaaca3", size = 1746213, upload-time = "2026-06-01T19:39:36.654Z" }, - { url = "https://files.pythonhosted.org/packages/b1/28/24e1409e605a9aa5d84abe0e2acb365354b70ae56d40948101cabe3341ab/aiohttp-3.14.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d33e61021222ce7f9792bcac870d6f58d8adfceda33ab857b01264f4560f2c5f", size = 1705862, upload-time = "2026-06-01T19:39:38.968Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/e5eb3ff1daeaf644c7e36a957517672494122628e067c38b263fa04eda77/aiohttp-3.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:44eca38755d0105bb32f47d085f5dd449846a449e1245fc105889e3279dcf8e3", size = 1798909, upload-time = "2026-06-01T19:39:41.334Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ba/8943f906f0570342886ababb9a722a44e360f786a028c5e0b0e29e3f735b/aiohttp-3.14.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f13087e06f68fea4941c21a0c541c00553aa16e4f8fd7bbe2b198df761e964d6", size = 1868892, upload-time = "2026-06-01T19:39:43.807Z" }, - { url = "https://files.pythonhosted.org/packages/3a/05/27df32c844b2156e1675a8d8ec22d963e3c8ba469ed7ceb1863320c7b521/aiohttp-3.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff82be7f1ef73634cb77890a770743239bc3d487b848669be1c599889336dc0a", size = 1751659, upload-time = "2026-06-01T19:39:46.398Z" }, - { url = "https://files.pythonhosted.org/packages/7f/62/da182e5910ab912b2e88aa919b61a16046a37a95714a5795b02eb57b2d18/aiohttp-3.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a150c0875ac8fd87f1c398650841308a30d65facf7416b12dbdb9cfdcbe5a48c", size = 1578775, upload-time = "2026-06-01T19:39:48.902Z" }, - { url = "https://files.pythonhosted.org/packages/66/e3/53c67097e8a5ce98625e91e3fa7f43c9c6940de680345d03b3509a72a078/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:edc01ea4e1ec5a1649a28866262bf24195889ff7b27bdd947029a6086741de9b", size = 1710090, upload-time = "2026-06-01T19:39:51.392Z" }, - { url = "https://files.pythonhosted.org/packages/dd/55/0e2732ca598c7a4dfe8a775662376d0ca2977cb1030e48386d4da5d9a456/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:540632bf882ff8fc88f2e1697be0761578e89e0d79fb4a8a6d65dc5da7e729d4", size = 1715016, upload-time = "2026-06-01T19:39:53.807Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/f0b73730798c9ca525afc30b39f1f81bbe24e245d9654c54d3b39d63212d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:860a86bc2c80237f5dff52edcf427e10a8d8352271fd84845429a3e60199e02c", size = 1763810, upload-time = "2026-06-01T19:39:56.31Z" }, - { url = "https://files.pythonhosted.org/packages/71/cc/11acb6c4518f448323405a7312b6f255d0f974a34373ad1db7633c4aadc8/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cbd50e6a50d6b99283a826b18cbdebf65b0797689a7535cb0e9dd37be0f63c3", size = 1573064, upload-time = "2026-06-01T19:39:58.718Z" }, - { url = "https://files.pythonhosted.org/packages/de/2d/28c31dde0a7dc98c0ee7d0da2ddcec3f7688c4fc131e5989e278d0c03c0a/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20144819e99db593e22bbd2f3f2691a5e149f879142d6b8670254708853ff4fb", size = 1775765, upload-time = "2026-06-01T19:40:01.195Z" }, - { url = "https://files.pythonhosted.org/packages/b8/69/155c4ef3aec96417d47024800472b33b16c5d8a665371dcd044c2afdf25d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26b6d79aa54cb4ed50cc7d41ed14e99e0f1fc8e7c2d42f2e05b37aea897b2b52", size = 1733716, upload-time = "2026-06-01T19:40:03.631Z" }, - { url = "https://files.pythonhosted.org/packages/5f/44/6126116fd8a316b712bb615660b855c78466bb67ba1bb1742427eafcf7ac/aiohttp-3.14.0-cp314-cp314-win32.whl", hash = "sha256:106ed074a856f3e21d186b8579e2c8afb6da598e267cdaab01059e13db2fc44d", size = 453684, upload-time = "2026-06-01T19:40:06.277Z" }, - { url = "https://files.pythonhosted.org/packages/a2/d7/eff4c58a88c5cac5e38b55f44fb8a6d3929c3cbd77356e383e094d3220bd/aiohttp-3.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f770846edae8f00ecc57af825bce811f787f87a7dcf0e90d191790efe5b31f7", size = 481758, upload-time = "2026-06-01T19:40:08.653Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ed/17b5bd9fbcb46e688f02e572f517754a9a75831e7b54702f027761dc4fa5/aiohttp-3.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:acf1581c4f21ed4b80a2dded504d87b055a071a84d5737ea966435f768275ac6", size = 450557, upload-time = "2026-06-01T19:40:11.03Z" }, - { url = "https://files.pythonhosted.org/packages/12/34/6180103ce9aabc8ebff3f7bb55a1228ffe60f61042823031d9692cb7b101/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6aa1a40f9cbb3da9f80714c5966b8946c21e6a2530d809b9498b33161e3c8733", size = 787878, upload-time = "2026-06-01T19:40:13.401Z" }, - { url = "https://files.pythonhosted.org/packages/92/e9/08954a40e8b7baa3d8beadd2b074b186e9b1e9c8ddabc288678a6265de50/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b62af5a8cc96a194eaa01a9ed7b34a3ffa58d3d8daaa1a0d7a749353ad12d228", size = 524400, upload-time = "2026-06-01T19:40:15.972Z" }, - { url = "https://files.pythonhosted.org/packages/08/6a/b5965a634ac4d5ba99a463314cf4ab214ca073fcdc38a15e0294273701fc/aiohttp-3.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6eb63b1417efaf7d1002a6ad034a40d44376afcc16508a57f8e74b49ad26a095", size = 527904, upload-time = "2026-06-01T19:40:18.28Z" }, - { url = "https://files.pythonhosted.org/packages/06/b4/932bcdd850c354d9bcca30f360e475d7852e30413fbbd44b182782ed5432/aiohttp-3.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c20b9ad156a79eb97be5cf9e069eec01d2f0dc8472ffbd75299a8b2d4c2cbbde", size = 1912162, upload-time = "2026-06-01T19:40:20.825Z" }, - { url = "https://files.pythonhosted.org/packages/c6/85/ce79bab0310d2e3fd2d7bc7e44412abeff7c8338f8a21dd0f2f1714989e5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40ae7b0642c25632c7eabc4a04754012691864d2a1b93becf7cddb76027b838a", size = 1778813, upload-time = "2026-06-01T19:40:23.726Z" }, - { url = "https://files.pythonhosted.org/packages/05/54/ba62ac2d1bc87e010aad23751e383b8794e45d931df67677313a2da78823/aiohttp-3.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95f5217e76a046b9f228a101717ef8d42b1eb3d9d196d15202db5bf41df88936", size = 1899969, upload-time = "2026-06-01T19:40:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/dc/82/7cc7907725d83a19f31551334061e1ab8e108b1d7ac52632a2a844a4acb5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a4a9f17e85b80878c176695c1998c790e83731d8271881e5d356488652a1f9e", size = 1991771, upload-time = "2026-06-01T19:40:29.061Z" }, - { url = "https://files.pythonhosted.org/packages/d0/1c/a57de71a4508c93a830b77c28af3d08cd97f606dedfc6b94275347744508/aiohttp-3.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:145262119b07d7f95abc1839add35ba2bfc84551d4b4660ca11542c0b215455b", size = 1868606, upload-time = "2026-06-01T19:40:31.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ae/3839726cd49150a53ed340cc24ce5ba09d4c2117020ef9d45542bec5eb2f/aiohttp-3.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49a33ded29b0b2fa7a367a02cf0fb89af602bb87542a16177ec8ce1c9c51d12a", size = 1665437, upload-time = "2026-06-01T19:40:35.01Z" }, - { url = "https://files.pythonhosted.org/packages/35/1e/c237923232c7da7f0392ea25d89fc5e60c0e93f685f4ebca8e7bcdd5271c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cc736a9c9fc2bc4dd71fd404815741b6573df27c3f985948ec4076989ac57de", size = 1834090, upload-time = "2026-06-01T19:40:37.733Z" }, - { url = "https://files.pythonhosted.org/packages/98/02/a5a7a2524f92d3911761b405a7c067c751891942144adc13e2ad79611e39/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4141a3e5342ee3053a9cab54d25b64ed28289c1041e4c54b3d99839314d90ce", size = 1816907, upload-time = "2026-06-01T19:40:40.46Z" }, - { url = "https://files.pythonhosted.org/packages/fa/76/a8b9f0d09234d516af9f2d7dd715557f33b5da3b0b56ead41d1170e86e3c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e30871b2d58996cb81aac52d2b1d15ac05257131ef0f90f18c2115a380fbfe7c", size = 1840382, upload-time = "2026-06-01T19:40:43.48Z" }, - { url = "https://files.pythonhosted.org/packages/c9/8e/140e715a0a4bbc211979ea30ec8396ad2ed5bf90ab87d8058fc4668b1923/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:667b881d083ccae3900ea5a241e17e5007ca78844c53ed389bb63d48f729d9c7", size = 1659497, upload-time = "2026-06-01T19:40:46.265Z" }, - { url = "https://files.pythonhosted.org/packages/10/c7/7ba5de8af9650b9767b063c675427b8685f43fa7ce563673a7bc3af60f08/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:b584dfe615d151e9b8f0a8ecb3aee6147f2927ec5b95ba25fe621f5377510928", size = 1870829, upload-time = "2026-06-01T19:40:49.583Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bc/2aaab2f85cadb26ea59c091fa2b8e370d625154b5c14b478f1b489d07551/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6199707cc40e0e9cd39c36fbc97bec416c704e1d0ddce03412bb3b3e6a90ccd0", size = 1832281, upload-time = "2026-06-01T19:40:52.303Z" }, - { url = "https://files.pythonhosted.org/packages/39/98/31b9ad9fbc01f0075ee7221002df5fd2d10b647f451ca5f30edc802d9dd6/aiohttp-3.14.0-cp314-cp314t-win32.whl", hash = "sha256:a8d93334d4961c9d566b1f046c81dee475b7c21eb730728d38237bfa70d1c8e6", size = 490597, upload-time = "2026-06-01T19:40:54.937Z" }, - { url = "https://files.pythonhosted.org/packages/59/1f/299b21441c8de42ff70fddc7cfe65e92f810abcf740739a09b56f7835364/aiohttp-3.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d2ffe9b614f50f069068b3b52e73414e4107fc10b7efc939a76acff9251fdd2", size = 525789, upload-time = "2026-06-01T19:40:57.306Z" }, - { url = "https://files.pythonhosted.org/packages/70/11/7f83fcba9ee05d4c54d61b3f8104da0d43a59adac44dd28effc0c9a10422/aiohttp-3.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7a3fc4358e65826c515350f199c210de747cf669998211b1ee6c2e46de364b24", size = 467399, upload-time = "2026-06-01T19:40:59.993Z" }, + { url = "https://files.pythonhosted.org/packages/6d/67/58ded4b3f2e10f94972d8928050c85330e249a31dd45a0e5f3c0e9c3fa05/aiohttp-3.14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e", size = 766140, upload-time = "2026-06-07T21:05:37.471Z" }, + { url = "https://files.pythonhosted.org/packages/18/68/4ae5b4e08943f316594bb68da89957d3baf5760588fa09509594bd777e4b/aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491", size = 519430, upload-time = "2026-06-07T21:05:40.751Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c1/316c8f3549dbe5245f92bfd523ec6f32dd4d98cafe21df3f6a19b1184c75/aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce", size = 514406, upload-time = "2026-06-07T21:05:42.111Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ee/fb0ac28684e8d753b83c8a4eebc19a5846912aa0a4daaabb6a9936363840/aiohttp-3.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3", size = 1703649, upload-time = "2026-06-07T21:05:43.427Z" }, + { url = "https://files.pythonhosted.org/packages/3b/57/aa2beab673331f111885db8a7b69dfe3ab0e53e446a0ace18ca694b4dc58/aiohttp-3.14.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505", size = 1675126, upload-time = "2026-06-07T21:05:44.897Z" }, + { url = "https://files.pythonhosted.org/packages/47/ea/dad128abe365e79be03b16ed464198ac73e0d257e8260c6f7d6f31cbef26/aiohttp-3.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521", size = 1771558, upload-time = "2026-06-07T21:05:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/63/f3/b5b4e10327cb85d34d24232c6b71b64602f190b3ccb238a043ac6b187dac/aiohttp-3.14.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd", size = 1856631, upload-time = "2026-06-07T21:05:47.844Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/93294c3045775c708ac8310eb3d3622a11d2951345ad590d532d62a1faa4/aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb", size = 1714139, upload-time = "2026-06-07T21:05:49.982Z" }, + { url = "https://files.pythonhosted.org/packages/29/c4/93067c85a0373492ce8e577435203c5947c454af074ac48ed4f3a1b9dd4a/aiohttp-3.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42", size = 1588321, upload-time = "2026-06-07T21:05:51.431Z" }, + { url = "https://files.pythonhosted.org/packages/c4/39/9ff91aaf02af8b7b8222a987466da539f154c3e01732c22b5f5a20a8ee66/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b", size = 1670375, upload-time = "2026-06-07T21:05:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e4/77452a3676b8d99ac1375f77691d6bf65ea6e9f4b201b82ef77c916dc767/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192", size = 1690933, upload-time = "2026-06-07T21:05:54.902Z" }, + { url = "https://files.pythonhosted.org/packages/7d/84/b0059a7c7fc05ea23f3bc1596ba91c12f79588b9450564a24cac37536d0a/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05", size = 1740798, upload-time = "2026-06-07T21:05:56.458Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3a/e2a513ecbfc362591caa51a7f7e011b3bfc8938b388ae44cd95560d36999/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe", size = 1576412, upload-time = "2026-06-07T21:05:57.953Z" }, + { url = "https://files.pythonhosted.org/packages/a1/10/08f1654f538f93d36dcac66310a06eefce4641cdafca83f9f0a5317be254/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d", size = 1750199, upload-time = "2026-06-07T21:05:59.488Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/d91b70c57d8b8e9611e4a2e52238ca3698d3dc1c2efe25b7a9bf594ac584/aiohttp-3.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966", size = 1699356, upload-time = "2026-06-07T21:06:01.131Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f1/15340176f35ff61b95dbe34020bcf43f9e624a2d7bbac934715ff97d2033/aiohttp-3.14.1-cp310-cp310-win32.whl", hash = "sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6", size = 458939, upload-time = "2026-06-07T21:06:02.86Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c2/a2f1ec5b37f903109e43ae2862268cfe4a67a60c1b2cf43169fcdff5995f/aiohttp-3.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df", size = 482583, upload-time = "2026-06-07T21:06:04.666Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/7b56f6732ef79530afaa72aa335d41b67c8d79b946995f0b11ad72985435/aiohttp-3.14.1-cp310-cp310-win_arm64.whl", hash = "sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c", size = 453470, upload-time = "2026-06-07T21:06:06.322Z" }, + { url = "https://files.pythonhosted.org/packages/26/dd/bf526e6f0a1120dd6f2df2e97bacfe4d358f13d17a0ff5847301a1375a51/aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2", size = 765225, upload-time = "2026-06-07T21:06:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e1/a2872aa55495a70f61310d411541c6ee23812d9a884e000c716e1bc3edbf/aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f", size = 518743, upload-time = "2026-06-07T21:06:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e7/c60c7b209e509cc787de3cea0550a518538cfc08003e1c1e14c1c63fff71/aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8", size = 514139, upload-time = "2026-06-07T21:06:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8d/614ace2f579702c9840ab1e1447fd8509e35b0b904f7196418fa2f57b25d/aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04", size = 1784088, upload-time = "2026-06-07T21:06:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/726e90f99542bf292f81a96a12cc4847deb86f3ccf62c6f4014a201f4d33/aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8", size = 1737835, upload-time = "2026-06-07T21:06:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4b/d176d5c4db9d33dacf0543102ea59503bc1d528af4cfd0b719949ca49389/aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6", size = 1842801, upload-time = "2026-06-07T21:06:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d6/5a99b563690ea0cbed912ae94a2ce33993a5709a651a3a4fe761e7dd973a/aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af", size = 1929992, upload-time = "2026-06-07T21:06:17.947Z" }, + { url = "https://files.pythonhosted.org/packages/76/7f/a987b14a3859094b3cea3f4825219c3e5536242564af6e3f9c2f6c994eb2/aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730", size = 1786989, upload-time = "2026-06-07T21:06:19.677Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1a/420e5c85a3e73349372ed22ce0b6af86bfa6ce16a4b20a64a2e94608c781/aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621", size = 1640129, upload-time = "2026-06-07T21:06:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/a7/80/18a592ed3be0a402cc03670bd72ee1f8563ddbe1d8d5542dbf868f274136/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee", size = 1756576, upload-time = "2026-06-07T21:06:24.8Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0b/8b3d5713373858ff71a617daf6e3b0e81ad63e79d09a3cf2f6b6b983939c/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573", size = 1754668, upload-time = "2026-06-07T21:06:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/9f/49/fd564575cf225821d7ba5a117cb8bc27213d8a7e1811162afb43ae077039/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7", size = 1817019, upload-time = "2026-06-07T21:06:28.297Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/e850c9ae6fc91356552ae668bb6c51e93fa29c8aef13398a10b56678557f/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf", size = 1631638, upload-time = "2026-06-07T21:06:30.242Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/3c337ba72451a89806ace6f75bddc92bafc5b8d53d90115a512858024b63/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85", size = 1835660, upload-time = "2026-06-07T21:06:31.943Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9c/9c18cf367a0498212d9ba7daf990b504a5e8ae064cda4b504e2647c89c03/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3", size = 1775698, upload-time = "2026-06-07T21:06:33.72Z" }, + { url = "https://files.pythonhosted.org/packages/b5/63/a251a9d2a6cb45065b2ddc0bde2b3dd10108740a9a42f632c66405a761a2/aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126", size = 458386, upload-time = "2026-06-07T21:06:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/17/ca/69274c51dcd6e8947d77b2806cf47a4a15f2c846e2cbeb1882547d3da283/aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5", size = 483406, upload-time = "2026-06-07T21:06:36.824Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/c25904f77690c3688ec140f87591ef11a0cfe36bf3d5c0f1f38056fb62b3/aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b", size = 452987, upload-time = "2026-06-07T21:06:38.371Z" }, + { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" }, + { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" }, + { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" }, + { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" }, + { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" }, + { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" }, + { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" }, + { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, + { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, + { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, + { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, + { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, + { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, + { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, + { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, + { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, + { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, + { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, + { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, + { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, + { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, + { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, + { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, ] [[package]] @@ -223,16 +224,16 @@ wheels = [ [[package]] name = "anyio" -version = "4.13.0" +version = "4.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, ] [[package]] @@ -273,7 +274,7 @@ wheels = [ [[package]] name = "awscli" -version = "1.45.22" +version = "1.45.41" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, @@ -283,9 +284,9 @@ dependencies = [ { name = "rsa" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/97/c991bbed8ae237252fc9248750082b7738c5327074458062453a5f4775be/awscli-1.45.22.tar.gz", hash = "sha256:b011c0f9df70f74b465e42e463f91d8eeb769da4774599b9f4462a25591a46fc", size = 1895180, upload-time = "2026-06-03T19:33:08.891Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/e7/dcf9d3e75e75eda69c9b0dae1bf854243e9c72b841106734de4dfed672f0/awscli-1.45.41.tar.gz", hash = "sha256:1a1e813d0a58ec0daa961449dd39aec975c08133d36230c9409dc61f17c6a64c", size = 1895173, upload-time = "2026-07-06T19:39:34.637Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/ae/08393904496778030f3460565a328504c6afbdc462d2061b8f29c18a4fc7/awscli-1.45.22-py3-none-any.whl", hash = "sha256:b8d09dacf32c17cda30f20950d4be21de1a0c167b4c3aabf8e81e5a9c2d240ff", size = 4647002, upload-time = "2026-06-03T19:33:06.88Z" }, + { url = "https://files.pythonhosted.org/packages/dd/93/eaa4f1ffa1fa3eaa5de647320737a388eb671efcdf44cd27c610b8daa411/awscli-1.45.41-py3-none-any.whl", hash = "sha256:98770fd9d99baa6a1133989746cbdf29431e83a7e9b7a2f6aa80b531f619bc43", size = 4637407, upload-time = "2026-07-06T19:39:32.155Z" }, ] [[package]] @@ -324,7 +325,7 @@ wheels = [ [[package]] name = "azure-storage-blob" -version = "12.29.0" +version = "12.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -332,9 +333,9 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/25/fdcf1e381922dbab8ba23d6fd78d397fe6cbac6b480310218834b7bc91fe/azure_storage_blob-12.29.0.tar.gz", hash = "sha256:2824ddd7ebc9056034ebc76b17971a38e9aa5835abb0d565b9700493f2a6c657", size = 611359, upload-time = "2026-05-15T03:34:59.865Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/48/84a820d898267f662b5c06f7cd76fdb8a9e272b44aa9376cef3ec0f6a294/azure_storage_blob-12.30.0.tar.gz", hash = "sha256:2cd74d4d5731e5eb6b8d5c5056ee115a5e88f8fdf22517b739836fda685018be", size = 618229, upload-time = "2026-06-08T11:45:35.575Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2c/6ddee6a3e42d0236ba9259e4df7fa97fdc415ff0802b736c634baaf4b285/azure_storage_blob-12.29.0-py3-none-any.whl", hash = "sha256:ccf8a1bcd5e49df83ab85aab793b579e5ba2eeea2ad8900b2f62ca3a37dc391f", size = 434823, upload-time = "2026-05-15T03:35:01.837Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0b/e106f0fd7fa785867d9ffcc47dc9e6237c0e58f51058473b777487a98edc/azure_storage_blob-12.30.0-py3-none-any.whl", hash = "sha256:d415ac50b67a8da6b3ae7e9f1014b1b55cd7aafa0b8d4ca9b380568dc7360423", size = 435610, upload-time = "2026-06-08T11:45:37.213Z" }, ] [package.optional-dependencies] @@ -375,27 +376,27 @@ wheels = [ [[package]] name = "beautifulsoup4" -version = "4.14.3" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, ] [[package]] name = "bleach" -version = "6.3.0" +version = "6.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, + { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, ] [package.optional-dependencies] @@ -405,30 +406,30 @@ css = [ [[package]] name = "boto3" -version = "1.43.22" +version = "1.43.41" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/31/32388d5ec332ffe81d8f3650860f94b66294009172d188c390cad58c6f5f/boto3-1.43.22.tar.gz", hash = "sha256:2a7fe12d8e0731bb8aa7c1e59b4ccc770fda031b8659c2f6f497393bdcec3051", size = 113203, upload-time = "2026-06-03T19:33:13.39Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/5b/e91af651bf4e902f86b9e2f8bc09ca20dbd1ad3c1e21b70bf34651cf3cee/boto3-1.43.41.tar.gz", hash = "sha256:0f56811f13677bfb4542daa0cce8532c95d9afd27b4ba7b681af36a0568624ad", size = 112677, upload-time = "2026-07-06T19:39:38.788Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/6b/c2fb3b91e849882df5426e68cc15eb2b5ba6ac28325ab2aa3a1065da5884/boto3-1.43.22-py3-none-any.whl", hash = "sha256:0597fb9fe1613e636ac55219a5a54ad0fcb7c15e6be32c799301f7fb53ff04e1", size = 140536, upload-time = "2026-06-03T19:33:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4a/babb2de16f6ff0688697d2eac7d95566151c255f6b08f547306f57dabfc2/boto3-1.43.41-py3-none-any.whl", hash = "sha256:f48f862d2720ea9203ed2d842d436b8eb2d459ea31654a7ad7c0756fdf36c6b2", size = 140029, upload-time = "2026-07-06T19:39:37.125Z" }, ] [[package]] name = "botocore" -version = "1.43.22" +version = "1.43.41" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/cf/840f1b8db16d45e3807c23d1ea779723eed1cd9cf3b6c49e16f372d2a777/botocore-1.43.22.tar.gz", hash = "sha256:b00de525e538289ed4a7a85263f1be4e47473c124cec87be6b23be49356bf745", size = 15458781, upload-time = "2026-06-03T19:33:02.882Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/a1/48a0f38b0cac8196764607c0bca7e3ca50d3cffc825087b743d3635413f2/botocore-1.43.41.tar.gz", hash = "sha256:27627d79af0df7dcb7ecf78d8d3d1310da09a5e9460be30bf759f1c2ed095ee8", size = 15647567, upload-time = "2026-07-06T19:39:27.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/6b/576a1b0f915871e35f14a33104f2bcae635f19c6a72486ba639db0d1fc70/botocore-1.43.22-py3-none-any.whl", hash = "sha256:ceec9f81d0891abe7b28ca2b2ee47e32de7b3360ad11e80d351470f015217379", size = 15141375, upload-time = "2026-06-03T19:32:58.324Z" }, + { url = "https://files.pythonhosted.org/packages/61/4f/19e0b97ce1801c66a4d1d35117f36240aa20f864338f093fccc23873a231/botocore-1.43.41-py3-none-any.whl", hash = "sha256:0cc6e79b30a2a98374f16a31cd9c7a9106a51b60650bd8c34cc8223f58ae6b8d", size = 15331199, upload-time = "2026-07-06T19:39:23.694Z" }, ] [[package]] @@ -442,93 +443,121 @@ wheels = [ [[package]] name = "certifi" -version = "2026.5.20" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, + { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, ] [[package]] @@ -542,119 +571,101 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.7" +version = "3.4.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/56/10a88e00039537d74bd420f0457c52ab8f58a1af56126e3b9f1b1c8c4724/charset_normalizer-3.4.8.tar.gz", hash = "sha256:d9bf144d6faf12c70d58e47f7512992ae2882b820031d6cef68152deb645bf2d", size = 151790, upload-time = "2026-07-06T15:27:58.477Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, - { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, - { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, - { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, - { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, + { url = "https://files.pythonhosted.org/packages/1e/55/f7590fd3a3b9c80b7106876bc868827304462cbf9b40b0f3d664c04c4e67/charset_normalizer-3.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad320cccde0bbe430fe3a0e5ef3f17b16ef7cfe496603d7b3a153aa204e093e5", size = 321984, upload-time = "2026-07-06T15:25:42.469Z" }, + { url = "https://files.pythonhosted.org/packages/08/b2/1d6ebdf9dfb4f3be6777bee1589eb6c3784a5fb7389ebc511b5ca649f540/charset_normalizer-3.4.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:825e5c2fb57d2065250e12dfeb414d780e7da242b371d5a78814d3ef40d63fde", size = 216216, upload-time = "2026-07-06T15:25:44.047Z" }, + { url = "https://files.pythonhosted.org/packages/91/b3/be7c7a3d28ae7ccbf9c51641337114457f507005474f0caee4b26abe2be4/charset_normalizer-3.4.8-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98a91c8dce056a858385cf147eb196ae4d2b35315c2bfc8151ff4027707247e1", size = 238408, upload-time = "2026-07-06T15:25:45.19Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/f6bfb15e3feeb47a180d0dffdaa81ff119c5380d86733a35c0e5a31de503/charset_normalizer-3.4.8-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f2bd1869ae6cd2793e51bfacebd1495595c1c007c2dfcfd6874b4a804f29bc7c", size = 233221, upload-time = "2026-07-06T15:25:46.608Z" }, + { url = "https://files.pythonhosted.org/packages/39/f8/a9af1b0e78a4d2a3974eac8289d97f83526346571f5ef3edda13bcff262d/charset_normalizer-3.4.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:511851c35e4ec8f4be773acd2c0222a7ec6b63971fea8988103632896f174b4b", size = 223559, upload-time = "2026-07-06T15:25:47.896Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d9/5b8d04939351d4b17047fb6a7fcb44460ccce66663759a1a17fad6d5c250/charset_normalizer-3.4.8-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bae1f52a68a5dcc1fcb65db69895638654be9a45716c47a5a192552a52925662", size = 207714, upload-time = "2026-07-06T15:25:49.034Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a1/5b691a5a3cfbc0696ec7be6a438ace83ead88a57024845593eb25fb1a39c/charset_normalizer-3.4.8-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f44633a518ef0889dc7e28dde2e18616f8e932e2d17d9c9ccdbbef40aa3a6794", size = 221394, upload-time = "2026-07-06T15:25:50.177Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d2/6d34df842bf58e0ecd0a5c71ded2596f32b506e606856faf7b8d88ba6c91/charset_normalizer-3.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0d08f2b7171517aa86ddf0eaba25bc7ac8576d401f2da8764403cf0e876f796b", size = 218970, upload-time = "2026-07-06T15:25:51.317Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/a13e5faa0de1cc86a001d1e6b5547688c1a0ca4f8858ff8803516d14ac73/charset_normalizer-3.4.8-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bde2fd87539f4a023ae8d699f51cd3d359ba79a42953fc03afde80bedf52ffc0", size = 209442, upload-time = "2026-07-06T15:25:52.507Z" }, + { url = "https://files.pythonhosted.org/packages/a9/67/8f2bb932f518d444ad75ca17ce6192c4324b95ee8b4c73072387eb2c96e5/charset_normalizer-3.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ba191a019f567339cff1cf861e311a5c442477d91f2ece3ad683f7b004976aae", size = 225986, upload-time = "2026-07-06T15:25:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/39ecee39ee858b0a07a3cff1f36c72a632b2fb70f46117042adbf675bdc5/charset_normalizer-3.4.8-cp310-cp310-win32.whl", hash = "sha256:dfce5e536054cf15a14576ecb08913e2e9ceff4cc1da2919694cdb2b45b5c64f", size = 150674, upload-time = "2026-07-06T15:25:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/c0/38/bd10fa06f989f65e9b7e4868179e31ffb5e473049bbbf37e22256ad7503f/charset_normalizer-3.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:06238276da3d880d858a4221d6b968bccc18ca3cbe71f6c5c2de86abb7929ffe", size = 162057, upload-time = "2026-07-06T15:25:56.259Z" }, + { url = "https://files.pythonhosted.org/packages/07/2f/16ff1dc626483a27ac40ca1ae350d01891db496eb574a26a9738e16052db/charset_normalizer-3.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:3731936f612754018aeb99fafc4381b6d80ed9165f2dba719c4e1afca0177048", size = 152815, upload-time = "2026-07-06T15:25:57.389Z" }, + { url = "https://files.pythonhosted.org/packages/94/b5/7f10929c45e2d0d0dc78bead98458c271af3e028e66b16441de88829a8b7/charset_normalizer-3.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ca832d525a2a52542048111cb44e0dea595b9ad53ad542020aa770f308427b92", size = 316812, upload-time = "2026-07-06T15:25:58.71Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b4/8e936a5e19d7e7b19db29aeed0988b481dc745eed3437829780f6ec98ce9/charset_normalizer-3.4.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab07eb7b564635602734c3ef0e8d2db9d59cbbce54f5dc42b8f11aa1f56ce364", size = 213580, upload-time = "2026-07-06T15:26:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/27/b8/dbc3b3c4796e4e29e193c5d7e100bb8ebfa66267994c01ba1eaaa3ebc474/charset_normalizer-3.4.8-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e88da593ea098194ea08f58924b716a9ae0c39058edad8c5c9b0bafa39fbd56", size = 235244, upload-time = "2026-07-06T15:26:01.402Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/808ff7eb8d344a174b89a388a4540fb86e486f608c16b9a2a023ddf6c9c6/charset_normalizer-3.4.8-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ca83574f63f71b485da1c50d4e17bcef7375a56b459a861562554f1fbaac1a4", size = 229678, upload-time = "2026-07-06T15:26:02.62Z" }, + { url = "https://files.pythonhosted.org/packages/c2/35/525d3e86af55a66073c02dbf9b6a720c74601489e4b4cb391cad0a4cb620/charset_normalizer-3.4.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9840cca6969a7e35498f1ce6fc32b7842500783d303b5ab254679a0f591093c4", size = 221015, upload-time = "2026-07-06T15:26:03.888Z" }, + { url = "https://files.pythonhosted.org/packages/e1/63/10f18541380f2d1c40ffda42835661aa9f88a1333e586d7b0f1d29869113/charset_normalizer-3.4.8-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:ca8776b012d464136c239f51133120db94397bc69f3faee404ff8c99827f8192", size = 205002, upload-time = "2026-07-06T15:26:05.32Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/28168f20737bb7d7d40d29cb82d2084cb3a8c19a4e1fef18f07069a24338/charset_normalizer-3.4.8-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23c40d25fe581d34ff0ef7ff1ca1527f5c51c5c6edf0b8384eeafac4a0438469", size = 217527, upload-time = "2026-07-06T15:26:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/8a/41/bc39417675dcca7151be665ec20f4b1b25ff3486d8eb2f3d7662ebc56082/charset_normalizer-3.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6809e40a8876eb4742676fbbb57056c0344ef2555e5597d4343f0b365953afdd", size = 216538, upload-time = "2026-07-06T15:26:07.836Z" }, + { url = "https://files.pythonhosted.org/packages/76/7f/e322a4f060a32aa9510d3c76689be7d58c62653b09fe156912471e17a7bc/charset_normalizer-3.4.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1bfd4bf262d6f26805cc6a32dae2554db66ef7533e6c62cc12644d3882be05e5", size = 206170, upload-time = "2026-07-06T15:26:09.269Z" }, + { url = "https://files.pythonhosted.org/packages/5d/64/07d0a02c401433ba18d57f55a96ffe7c701a11eac57c74bae0e1506cfa40/charset_normalizer-3.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9421549d803f0f712aa6e1496d15ff5ac510fc4c7104e7e299ea1d498d582d59", size = 222807, upload-time = "2026-07-06T15:26:10.476Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/2d132bdad9cadc3251bbad2813d29642c068855ad0394234f76845672100/charset_normalizer-3.4.8-cp311-cp311-win32.whl", hash = "sha256:9a2748f7588b0c1e5166db3a9448fe86e392479aded89b0c86382d41cade91a2", size = 150195, upload-time = "2026-07-06T15:26:11.654Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5f/c46567479049f46929d8ed9e628990c2419f369c5ea5797add417808d37b/charset_normalizer-3.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:2e9f0db12ea28a3349e514443fc56d4b1fb3c81d0f9c0e33dacfdfc2ac63f774", size = 161151, upload-time = "2026-07-06T15:26:13.111Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ac/39a8bc03f7550f86f8c43205f2c97bc3a4cea775e5a0c0317917679cdef5/charset_normalizer-3.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:4b1f25c6e376033f31463d66f8bb01ae77a128ca11c6677c69ed8e9ce913bbf9", size = 152392, upload-time = "2026-07-06T15:26:14.335Z" }, + { url = "https://files.pythonhosted.org/packages/59/c2/39de60ef5687662f467bed3d1e6944c67a4f0d057141d0404002b8f405ae/charset_normalizer-3.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:faac37c4904598daa00cb4c9b32f3b4cc814fb5f145d7a531ceb4a70f2114132", size = 319040, upload-time = "2026-07-06T15:26:15.854Z" }, + { url = "https://files.pythonhosted.org/packages/a7/57/a9474c3aeaa337c8a330c0dc5df266527d56da3b189c029529f6b08af2a4/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f191c19a32dc6cec0fb8079789d786254653a9ce906fcab04ccd2eed07bba233", size = 215541, upload-time = "2026-07-06T15:26:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/13/a9/be1ff7e81f6e086dced2a7a7a28b789be351d9796084ccaf6136a4ffafb3/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05811b76943d477bb90822dedb5c4565cef70148847a59d574e2b35043aeb563", size = 236913, upload-time = "2026-07-06T15:26:18.482Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/d8c5eae93da26d463f9ebe46a4937ca44434dc2937a565b92437befb3d94/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3868a3e4ec1e40b419e060d063f93eac6f046fa21426c4816421223ae7dc8ab8", size = 232815, upload-time = "2026-07-06T15:26:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/98e301ca944bcca5e6bc312406b579c8a6d81546c1b494afb3a9478495d6/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25f93d194eb6264c64416cabff46a91f6d99b97e7525a1b4f35c77a99e75cc68", size = 223995, upload-time = "2026-07-06T15:26:20.931Z" }, + { url = "https://files.pythonhosted.org/packages/fa/df/f5222366b76dcb31453a9bd922610c893540d0e729fd390439b0d3e972ee/charset_normalizer-3.4.8-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:ee6a62492f18d432cca031fabd158f400a8c25bf7b9458f50953393a2a23d97a", size = 208522, upload-time = "2026-07-06T15:26:22.214Z" }, + { url = "https://files.pythonhosted.org/packages/74/52/293220d59d8ddfb8aa56836b33bd6df58e70795d8a102a858c2984480f00/charset_normalizer-3.4.8-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c16cb4fc35e4b064f5ee78d849f15a550ada1729c3372916672e38f1f01d1d4", size = 219660, upload-time = "2026-07-06T15:26:23.493Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2c/81a298e66f3d01e61bfc6f7064bbb553b067a9f1d979e5962bf00733069b/charset_normalizer-3.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2fbd0edb0426ab28e70fac9d1d4ef549eb5a64a2521f0428c441d75e4387e6c2", size = 218230, upload-time = "2026-07-06T15:26:24.629Z" }, + { url = "https://files.pythonhosted.org/packages/0d/45/f1dd2328cbc3340705f82072c09bd4c68d6e079191cde05810c1eac77eee/charset_normalizer-3.4.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccb9052771216170015f810b88065fd9e13b1e0b391f92abb9b47e0919a42aad", size = 210006, upload-time = "2026-07-06T15:26:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/28697000620e117eb413424caaf60b6f98ddb1b09b2c11f7c0038d9936a7/charset_normalizer-3.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3809ba5d3cd02aca0894597f2669a825bdfe2229061515c128b0f4e5533b4ab5", size = 225771, upload-time = "2026-07-06T15:26:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/12/a7/d5844e315f5f35e7938c415f07a1df144eed1cf993f1b43cc16c980c5b46/charset_normalizer-3.4.8-cp312-cp312-win32.whl", hash = "sha256:de63c31666a049f653ada24e800192e3c019e96bc7d70fb449a000bccf26a36f", size = 150922, upload-time = "2026-07-06T15:26:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/9b/82/eb8b72f184b1e4986dd9daec15d7f6d9285a6728d2b07b7f04656829f473/charset_normalizer-3.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:14a4bbe066f3fb05c6ba70e9cf9d34614b57a2fd70ea8c27cc30f34155e16a58", size = 162294, upload-time = "2026-07-06T15:26:29.745Z" }, + { url = "https://files.pythonhosted.org/packages/09/5a/ab810134aa41034a08ffe94c058102016e6ad9bce62f3cdba547b4723385/charset_normalizer-3.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:2b5b0c0dca0a02c3f816f89abf18af3d20416dedbc3d3aa5f3981045f88ae7b0", size = 152409, upload-time = "2026-07-06T15:26:31.034Z" }, + { url = "https://files.pythonhosted.org/packages/11/49/fe5a8572a70cd9cba79f80af9388ac8c5c914ed4459b956f940244e499a5/charset_normalizer-3.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:057f8609f7341618c98e5aa9a6109fa116acff2a658497d47ab3325b5e8f2b08", size = 317424, upload-time = "2026-07-06T15:26:32.23Z" }, + { url = "https://files.pythonhosted.org/packages/e5/59/d71c96616b6825425a876f79f38fa440db30b32cc1166179a839f6259150/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c591f9a82adc5b89a039b90df74e43de2b9177fb46771172bed7b80722a70db0", size = 214723, upload-time = "2026-07-06T15:26:33.635Z" }, + { url = "https://files.pythonhosted.org/packages/c6/35/dc9eeb297f19b7b6ada39709ccb74937e6c51f0947958ae601a977cedd5d/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b56f449132d9adefe55b87635d05177a914ed5d070438a74725e1d77a280002", size = 236200, upload-time = "2026-07-06T15:26:34.99Z" }, + { url = "https://files.pythonhosted.org/packages/0a/37/6775fe852b4acad8bf7e0575fbe8aa9f41b546e33251acbded3c04a6b0d9/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1004c3b5831a301dadfb9e916f38e78e2ff3e08db24a1ad7c354db8ee3dea9c3", size = 231740, upload-time = "2026-07-06T15:26:36.498Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/1bcc3f5f3bac81532384adcfcdd9362c7f46a188a19deacc1ddaf7bdaa00/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60883e22821d17c9e5b4f3ca1ef8074f766e3db28791f851b665929c515635c0", size = 222888, upload-time = "2026-07-06T15:26:38.161Z" }, + { url = "https://files.pythonhosted.org/packages/47/81/9f3993ca62ef090c58059da641e49e3129e74700a6a3beb58436cdb8d4b9/charset_normalizer-3.4.8-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:21e4dbb942c8a6342e2685f232dd2a7bc73465697bd26ead4f118271d28be383", size = 207649, upload-time = "2026-07-06T15:26:39.628Z" }, + { url = "https://files.pythonhosted.org/packages/11/f4/679f636bcbdc2d53d06b1f4039be310450dca95a9f76bbf22f09985556e8/charset_normalizer-3.4.8-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84bcae14c65e645ca66b661339183d32b8c846a17c96e3e81ab3d346e1c498d4", size = 218908, upload-time = "2026-07-06T15:26:40.911Z" }, + { url = "https://files.pythonhosted.org/packages/7e/44/96e8c81867ba8a45ff893c8e7474c2d6b9633f7aa663da7901d040214d3e/charset_normalizer-3.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4e4d44b8287aa13a25e16e29393d494b0643b24894f7c8266c6f6788dd36337", size = 217096, upload-time = "2026-07-06T15:26:42.148Z" }, + { url = "https://files.pythonhosted.org/packages/a5/bf/4d53f04f29bdb22601701f4f9f4d038edfb27976c296fcb7400c02736a6e/charset_normalizer-3.4.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f68545d1b267dbfafd5d253b6d1cb161562c4e61ab25b5c4cdb7d9e5923e441e", size = 209355, upload-time = "2026-07-06T15:26:43.557Z" }, + { url = "https://files.pythonhosted.org/packages/ad/60/92b3f630798d777fa880ad289a3f9f2fc663e4b4beb24783c53318820254/charset_normalizer-3.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1936e48214adea74922a20c8ab41b1393ae27cc9e329eb1f0b937d3416824f36", size = 224732, upload-time = "2026-07-06T15:26:44.892Z" }, + { url = "https://files.pythonhosted.org/packages/bd/85/eafa0a3c7bb6fe9f02f4c7901f02071933cac85ee634197e17280818c6de/charset_normalizer-3.4.8-cp313-cp313-win32.whl", hash = "sha256:1f8e3521860187d597f3867d8466da225b9179ea2833bb26de1bb026144d07c3", size = 150358, upload-time = "2026-07-06T15:26:46.153Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8c/879fafff7b47bb1166d289f2d2472cb31b9922f9f4ca1f392edf85ec16be/charset_normalizer-3.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:8b654b6f52a0a9a6be38e88f3e1dc68f1093ebeb2abbadafc7c82da0786a34be", size = 161685, upload-time = "2026-07-06T15:26:47.423Z" }, + { url = "https://files.pythonhosted.org/packages/c3/46/b57c7e778a7b578f28d35fd38544687d4f8d9c019585eebc5ad936073fad/charset_normalizer-3.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:d2d5a250ee26e29468b7607d97479221b069fa8aaf6f929ac84ec0e962e15154", size = 152333, upload-time = "2026-07-06T15:26:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/0a8540b8cd494951cca1428606373942803f5ffcec40fe798f819c5a8adb/charset_normalizer-3.4.8-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:77e993ecf65f21ab1f82266ff5e84a7de2c879e7d9b8bc006009df83f22a1d5e", size = 316993, upload-time = "2026-07-06T15:26:49.962Z" }, + { url = "https://files.pythonhosted.org/packages/0e/99/a0868f0a1f0a045fd374d1f2cf7042d8ad5d7fb4dd1f4ac7365e319f7e32/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:524939917f17f6de502dfda30b472550965740d7f126659d4c4f8dd1569cce22", size = 215638, upload-time = "2026-07-06T15:26:51.338Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/43c4d09a09b5557cc5fe1d87c9d96f86a3942aec0517d2b5408cef87ca75/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a4508989ba8e2ce43ef989453d18188b261546e8188cbdd4ef451fb9e4c3b467", size = 236456, upload-time = "2026-07-06T15:26:52.531Z" }, + { url = "https://files.pythonhosted.org/packages/e2/67/492ca98b3ab785b736b5da10c1bc233e1c8fec6c0cdb29b482c38bfc52a2/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9e44127f7d11eee4548ad2cdf1f4e1b6eaaddd5cb92d15ad65f6ecc9bcf403ab", size = 232253, upload-time = "2026-07-06T15:26:53.838Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/1e6eff58c14f1aace1e26d80defbeaea2d35e075dbe4b611111ee4b47fa8/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb90317359f7e67bb6df615999a95e0980877468e617ddce8b6c2f8e7fe60d95", size = 222886, upload-time = "2026-07-06T15:26:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/40/7a/90056a5326b0c4b9a3f924d337729c344c11542e5bc7191e50410db61587/charset_normalizer-3.4.8-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:35d9e7a9c960520ae89d1f4e305d1c047a74dea2e0f73a0e84f879356c2e8776", size = 206482, upload-time = "2026-07-06T15:26:56.306Z" }, + { url = "https://files.pythonhosted.org/packages/18/ff/94761d31a33878dbb5008ddbd918615061fcf5c0a612aa3075450e60f628/charset_normalizer-3.4.8-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:92e322b054c7ff886f78feab7360736bb45de2e18cf4a0ee84e8fc5a08d53a19", size = 218929, upload-time = "2026-07-06T15:26:57.422Z" }, + { url = "https://files.pythonhosted.org/packages/2b/dc/00b9675acd7c4b926b9102ee3f0d1a570ce943901be73b87485001393fe1/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c0086d97094363556206dc3bcf43f7edcfc043ea7a568a46f45efea74858bd1", size = 218069, upload-time = "2026-07-06T15:26:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/04/11/94ada5a0482ee4bf688d04be4c7d6fd945d37370d04a95671040dfe2b416/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0752c849b51198267df2aba013c4de3a2955bd014a4fd70828809946c1acbc0c", size = 207146, upload-time = "2026-07-06T15:27:00.058Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/246bd36762207ab4752cd436b64e5d81a1668b15ddea7b5b2d0e8545e727/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2a4707e09eca11e81ece4fced600c5a0a801f568b962244f6f517bc274745fc9", size = 224896, upload-time = "2026-07-06T15:27:01.599Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f7/3510622d1fbe13b0ebf827c475e40a27e2be427140d792878b63ab6425cc/charset_normalizer-3.4.8-cp314-cp314-win32.whl", hash = "sha256:8ea67f427c073ae3da0923aa55f3715131fa613a61a7f2f8d762bde75eaf00ae", size = 150851, upload-time = "2026-07-06T15:27:02.964Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/9ce65dd21672b55cf800cca5f4433afa1586fda1d78731067ec9ec544c62/charset_normalizer-3.4.8-cp314-cp314-win_amd64.whl", hash = "sha256:ff71018850863362e5c7533769d0a9f77715c31af1502d523630ce822922f5c9", size = 162549, upload-time = "2026-07-06T15:27:04.249Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/9a5967eed666a88f31a0866884606d9ec3c2cd6091e2ccd7e0b4c4176c35/charset_normalizer-3.4.8-cp314-cp314-win_arm64.whl", hash = "sha256:44464e66f4da2f21dea7145c7693f9f60717ca4794a954dea5bf8c2c932678bd", size = 153079, upload-time = "2026-07-06T15:27:05.608Z" }, + { url = "https://files.pythonhosted.org/packages/02/4f/aa44cc81d8987f105352c74c0bf919007f8b80e9880d28bcf0393c1a816e/charset_normalizer-3.4.8-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:50a0c2e58ad2c203adb616fef28941b7e13716adbc25e0dfaeec29f5afe6382f", size = 338586, upload-time = "2026-07-06T15:27:06.86Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2b/b0392e2b235c08ff0623d905c2ee8ac820620544043c1ce92ce0b3d64c55/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1e589fdb95c76f08288bbb346230cdd8994db74903db6637b380f7b5fc9336", size = 222764, upload-time = "2026-07-06T15:27:08.23Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7d466879190731f5559662c22232646f2ae2dace2323c3e5aefcf78d458a/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3d7c887444c5a7ef0d68d358d81e758a850bc626f8e639e2ca5667153272b20", size = 241331, upload-time = "2026-07-06T15:27:09.512Z" }, + { url = "https://files.pythonhosted.org/packages/70/17/8b89e797137aa28c8fb0bafbafc243246a7afe21620a13b00e37624ece1d/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:65c389b96c0cfff3a3f0458fa1c7ce554a30e23101a88a49f03997afce6a929f", size = 239323, upload-time = "2026-07-06T15:27:10.86Z" }, + { url = "https://files.pythonhosted.org/packages/d7/98/1c1940730ed22d50983be4e243c722c89d5136d6f073bd840d1128bfddcb/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:593403fc47dcdf55e2987b2e3cc2e064127e2b908929f1f18b2e4a4652cbd780", size = 229964, upload-time = "2026-07-06T15:27:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/53/2d/bb8e81b7ff603d3f77e9a8a5d1ad34fcabbf3c54d300c29d99fba581fa23/charset_normalizer-3.4.8-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:606088e9fa2b7469ab9c42d4da8e05a415622a07714edd2fcd8fed48dda4c853", size = 212405, upload-time = "2026-07-06T15:27:13.447Z" }, + { url = "https://files.pythonhosted.org/packages/ce/1f/e52a3a53b13da591bb8f21d29e63877268eadf20686b7762351d4b89062c/charset_normalizer-3.4.8-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0317406326fed512f42a1632ad91a96228a7616c06547666a6dd79967f1bd6ca", size = 226918, upload-time = "2026-07-06T15:27:14.89Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f9/32996d79c57189af9722fe618f46d8a86b7be035ca98887b8d0c3821f141/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b67d50ee47e5c57a0064a9cb575b963a7125819dfd1fd094d44d378fff94659b", size = 225113, upload-time = "2026-07-06T15:27:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d2/9248c18e695696513774523a794cfb8b677521ce9ad7554d301cb10a9b20/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79e402b869f270140afa5e2b0e2ac100585358d812fe3dd093d424f7a72964e0", size = 214966, upload-time = "2026-07-06T15:27:17.418Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9d/4b19432d406179a40f924691906ee5b15ac664b408971c973295192444ea/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2970b9f7ab69ec3a0423ec6b6ac718e79fbf4a282c0bc103ef88c1ef50dfa15a", size = 231699, upload-time = "2026-07-06T15:27:19.131Z" }, + { url = "https://files.pythonhosted.org/packages/be/41/bdbdf71e8c3ccff10ef3cc2bb9467a7fdb3dc94b9a406d1a3c44afd39632/charset_normalizer-3.4.8-cp314-cp314t-win32.whl", hash = "sha256:458c2972a78043b7261c9726670029f15f722e70669bcbe961153a01968f589f", size = 155333, upload-time = "2026-07-06T15:27:20.681Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f8/e05c69323bd50091ec39f5f885385b884624b0131a6885a0c83a6217ba7a/charset_normalizer-3.4.8-cp314-cp314t-win_amd64.whl", hash = "sha256:0c926329a1df7cd56d7d8349fe354460d20aefd2e394c9e159e479d018b2b359", size = 167378, upload-time = "2026-07-06T15:27:22.042Z" }, + { url = "https://files.pythonhosted.org/packages/c2/04/cbaf1a2f5e2bbf70760e774380cbf052b10849fc35e770905df31af5cf00/charset_normalizer-3.4.8-cp314-cp314t-win_arm64.whl", hash = "sha256:2232baea80a2b01783679fed4e625ccdb19a974f44c9cf0fba21a777a4c8179c", size = 157782, upload-time = "2026-07-06T15:27:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/23/52/d5bee5b6ea81882d549b566d2545b044bbcbc33fe5fbe001008a7e745a21/charset_normalizer-3.4.8-py3-none-any.whl", hash = "sha256:b7c1fb310df524e01fbe84d43b7f95aa4f808f8eaa0dafc185f64ba395e37d54", size = 64279, upload-time = "2026-07-06T15:27:57.043Z" }, ] [[package]] name = "click" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -668,7 +679,7 @@ wheels = [ [[package]] name = "cohere" -version = "7.0.3" +version = "7.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastavro" }, @@ -680,9 +691,9 @@ dependencies = [ { name = "types-requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/c0/3c9b38c7332dfb1523a079fa3c5ea701a5ea82997d12d057c4002b6a0c75/cohere-7.0.3.tar.gz", hash = "sha256:f74c6be98ee5d0d1310c3892171def2f184d5a9b7ab81d906407ffc7a5e62adc", size = 208811, upload-time = "2026-06-01T21:52:38.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/23/cbb8ef34b8395ecea3cef2464d6abcb25174853fe8ad948841204f81f9a8/cohere-7.0.5.tar.gz", hash = "sha256:7fc682bb5c408402fc5ce59322bfa3f6aae27bde1ecc2450b0a25370040707ed", size = 208820, upload-time = "2026-06-29T20:30:53.132Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/0d/f543e349da10761c498e3a773e728c2d6b1aeb47195f2f09c6145452d51c/cohere-7.0.3-py3-none-any.whl", hash = "sha256:c880a45c4cd670d5e7c9297de122005f21e2d9624fda9314cb3e4e0406bc9ec5", size = 351965, upload-time = "2026-06-01T21:52:36.429Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/64ca464baa34d0fa8144fc09ec7a35c8fcdeea1be878a518e901b0a6032b/cohere-7.0.5-py3-none-any.whl", hash = "sha256:fd98f82be28c969da7625adddede0bb85f8f84de399daf3a98c40c0e7b93e104", size = 352049, upload-time = "2026-06-29T20:30:51.472Z" }, ] [[package]] @@ -696,23 +707,25 @@ wheels = [ [[package]] name = "colpali-engine" -version = "0.3.16" +version = "0.3.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "peft" }, { name = "pillow" }, { name = "requests" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "torch" }, { name = "torchvision" }, { name = "transformers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/af/b96dba065e0be5eb71add1813b95da0a67c85dbced9af37322907894a64d/colpali_engine-0.3.16.tar.gz", hash = "sha256:436b5bcb3c77c3eb90391fcb1d1c4576dd196732dd64c9e4c234cb57abf58c31", size = 195672, upload-time = "2026-05-12T16:32:50.987Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/b6/2346706c984456fa78110edd5c24ade2aba03853152a68b7fbde82b00197/colpali_engine-0.3.17.tar.gz", hash = "sha256:bffebf23a04d1bf081e7420cfe7d5f6e356bd03e3ae8438282fa4da1bbb5bb6c", size = 204054, upload-time = "2026-06-08T14:11:09.136Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/1e/e9f5a85a91e7f1dbb3e9b8874bc9c9fe4d6baad3e5a664345128cdfd1eb3/colpali_engine-0.3.16-py3-none-any.whl", hash = "sha256:6c252f564c3fafefef34fc931352af4f0f0fbbdc737ae2b58c683c972e931da6", size = 108858, upload-time = "2026-05-12T16:32:49.113Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2e/d703c21fe64ed562a8f163f73774f0fec684fedcefb0d8b73b4394eff68c/colpali_engine-0.3.17-py3-none-any.whl", hash = "sha256:362784700c532449c29424890e8ebe6b329307e9e9ea4906b0834d5b331c6dfd", size = 112292, upload-time = "2026-06-08T14:11:07.841Z" }, ] [[package]] @@ -726,62 +739,59 @@ wheels = [ [[package]] name = "cryptography" -version = "48.0.0" +version = "49.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, - { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, - { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, ] [[package]] @@ -808,10 +818,10 @@ wheels = [ [[package]] name = "cuda-pathfinder" -version = "1.5.5" +version = "1.5.6" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8fc9b0cdc5b7f62746e6a01b85b6461e5ae27f871010a5fcf8fa6950766d/cuda_pathfinder-1.5.6-py3-none-any.whl", hash = "sha256:7e4c07c117b78ba1fb35dac4c444d21f3677b1b1ff56175c53a8e3025c5b43c0", size = 52972, upload-time = "2026-06-30T00:58:04.34Z" }, ] [[package]] @@ -941,11 +951,11 @@ wheels = [ [[package]] name = "distlib" -version = "0.4.1" +version = "0.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/b2/d6fc3f2347f43dada79e5ff118493e8109c98400a0e29a1d5264a3aa479b/distlib-0.4.1.tar.gz", hash = "sha256:c3804d0d2d4b5fcd44036eb860cb6660485fcdf5c2aba53dc324d805837ea65b", size = 610526, upload-time = "2026-06-02T11:17:40.691Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/18/3497c4fa83a76dcb154923fd2075522e8dd6995ecee4093c00ae18160046/distlib-0.4.1-py2.py3-none-any.whl", hash = "sha256:9c2c552c68cbadc619f2d0ed3a69e27c351a3f4c9baa9ffb7df9e9cdc3d19a97", size = 469216, upload-time = "2026-06-02T11:17:38.779Z" }, + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, ] [[package]] @@ -968,44 +978,44 @@ wheels = [ [[package]] name = "duckdb" -version = "1.5.3" +version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/00/d579dcb2a536b6ea3a2563cdad6844f77d81a9b2d4b22a858097f2468acf/duckdb-1.5.3.tar.gz", hash = "sha256:df39428eb130faa35ae96fd35245bdeae6ecf43936250b116b5fead568eb9f16", size = 18026640, upload-time = "2026-05-20T11:55:31.901Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/29/9bad86ed7aa812d8c822a27c15c355b6d5423b991feeec86ed18027b6daa/duckdb-1.5.4.tar.gz", hash = "sha256:f9e32f1cdd106793d79d190186bed9e75289d51e68bd9174e47c04bffedeab6f", size = 18046634, upload-time = "2026-06-17T10:48:52.499Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/4a/06a81e9e3c01634760073b6a8911fccee2e33b23569f479971f910228972/duckdb-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6ef8faf121d7b3ad95aab1c3ce31169a28be49da75abfa6099a1bec2e9a70189", size = 32575938, upload-time = "2026-05-20T11:53:56.365Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ed/03ccf15147db7468f65891b5daa8489aa755d8cfcf75b24e7a4e4720e28c/duckdb-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd00f70231951a619908471b7b6397232ff3be8ccd1f49a47f1a2ccac59eaba1", size = 17276167, upload-time = "2026-05-20T11:53:59.558Z" }, - { url = "https://files.pythonhosted.org/packages/80/bd/ffc9e7a52731eec047dd49d45f029580dc8f3a6f8e8802a9e1c4138b0f8a/duckdb-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50379b85f3a0a169478d54880ef8bf971ecaa85772d05eeaa617d720c7704741", size = 15425037, upload-time = "2026-05-20T11:54:01.894Z" }, - { url = "https://files.pythonhosted.org/packages/2f/5d/44209bcd4ad47feb84831edd3f269397e03167efbb86aee2d102b2f71252/duckdb-1.5.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d37650ec3ec8a951400ea12dc77edaea88e0baeda34801792776f95f2f922f4f", size = 19306975, upload-time = "2026-05-20T11:54:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/76/5c/bdb735011131c429cdb6d9e12e12fc8f1c8622158890d76a25819a58efc2/duckdb-1.5.3-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3fb3bad9bc1a3e101d66d33269142ce075dc3d75202ba74ba97d7e44c50b9cd", size = 21416979, upload-time = "2026-05-20T11:54:07.429Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c2/929666ff6d71b15b128c6f800d893fb5589c7b6430ff9ecdcd4e6d175bea/duckdb-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:fdc65233f0fcf9022e4c6a8ba2ba751a79deb291501073d660afb1aa9874051f", size = 13101915, upload-time = "2026-05-20T11:54:10.072Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fc/a8a89c6c73f31c2b58c6abbc2f543e0b736042dd5ef7cc1784c24ec31428/duckdb-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:341a2672e2551ba51c95c1898f0ade983e76675e79038ccb16342c3d6cfb82d7", size = 32583465, upload-time = "2026-05-20T11:54:13.132Z" }, - { url = "https://files.pythonhosted.org/packages/63/f1/3423a2f523dd034e505d4a5dd8e210ae577212e152598dc13b6a5e736e1b/duckdb-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c9e8fa408705081160ede7ead238d16e73a36b8561b700f2bf2d650ae48e7b92", size = 17278520, upload-time = "2026-05-20T11:54:16.368Z" }, - { url = "https://files.pythonhosted.org/packages/e1/1a/7bf5ba1b7ea520557e6b2dbee1c85abab016bdac0c1779d9d0ef76c87300/duckdb-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70a18f932cf6d87bd0e554613657a515c1443a1724aacfc7ec5137dd28698b03", size = 15424794, upload-time = "2026-05-20T11:54:19.891Z" }, - { url = "https://files.pythonhosted.org/packages/ad/16/ce4b1e386e45fab0268edbf1b85bace20e9437589e9edb2bd5f9a226fa44/duckdb-1.5.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e80eb4d0fb59869cb2c7d7ef494c07fb92014fe8e77d96c170cd1ebc1488a708", size = 19306666, upload-time = "2026-05-20T11:54:22.77Z" }, - { url = "https://files.pythonhosted.org/packages/99/1f/651f8453f26931e8061b7e27b3090f868868185814ecb9216d0bd71ec8ef/duckdb-1.5.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3248b49cd835ea322574bc6aac0ae7a83be85547f49d4f5f5777cb380ee6627f", size = 21418306, upload-time = "2026-05-20T11:54:25.616Z" }, - { url = "https://files.pythonhosted.org/packages/bc/64/e1ffebf010b1631a6fef8d1508f46d4eab3e97c18729af986bb796fa8452/duckdb-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:f4eff89c12c3a362efa012262e57b7b4ab904a7f79bad9178fe365510077abe8", size = 13101423, upload-time = "2026-05-20T11:54:28.107Z" }, - { url = "https://files.pythonhosted.org/packages/e7/42/b1d4e34f9658cc0e13d7aae581ab82643f50a548d5aee8767f0c587cc3a4/duckdb-1.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:75d13308c9da3ee431d1e72b8ab720aa74a1b3e9159d4124cb62435924496334", size = 13951740, upload-time = "2026-05-20T11:54:30.886Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/2e34929b16c8d544ef664fad8f7f3a2a9db05746aae1e7c8c4ee3a8b23e4/duckdb-1.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ff11a457258148337ef9a392148a8cdbd1069b6c27c21958816c7b67fe6c542d", size = 32626494, upload-time = "2026-05-20T11:54:33.738Z" }, - { url = "https://files.pythonhosted.org/packages/3a/53/3af681793d03771365ae3e2215331151c196a3ac8193f613344840694671/duckdb-1.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fd25f533cb1b6b2c84cc767a9a9bab7769bb1aa44571a2a0bfc91ac3e4a38ac", size = 17301121, upload-time = "2026-05-20T11:54:36.928Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/c80af1eac2ab5d35fc2c372ef0a84668842e549fbbf7799277b3fccf3e39/duckdb-1.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10960400ed60cdf0fe05bab2086fa8eb733889cb0ceca18d07ff9a00c0e0be7b", size = 15449283, upload-time = "2026-05-20T11:54:39.777Z" }, - { url = "https://files.pythonhosted.org/packages/2d/9a/c63af233c9f761bf5178a5210437e1bc6bcb30fa8a9073de6398cfb12c03/duckdb-1.5.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5f18e7561403054433706c187589e86629a7af09a7efc23a06a8b308e6acc68", size = 19332762, upload-time = "2026-05-20T11:54:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/21/cc/2d77af4fff86012f334ef82e6d54a995a86c8745e58074f1218ed7d25171/duckdb-1.5.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fb7516255a8764545e30f7efacea408cc847764a3027b3b0b3e7d1a7bebbc5c", size = 21453290, upload-time = "2026-05-20T11:54:45.272Z" }, - { url = "https://files.pythonhosted.org/packages/8d/5e/9bc4817a98feb4dab83e56f2245cd3a30d00ee646d4dec7926464e2b3f28/duckdb-1.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:8001eccbc28be244dfd04d708526f34ddd6460b47a8aeb5d0e39d6f7f9e3fe15", size = 13118308, upload-time = "2026-05-20T11:54:48.058Z" }, - { url = "https://files.pythonhosted.org/packages/81/35/e3f32e4e53e2450ddb1db8312a17d1ce455d60cc4941b6ad2cfc908794b0/duckdb-1.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:6d2835e39bb6af73891f73c0f8d4324f98afe00d0b00c6d34b2a582c2256cbb0", size = 13927187, upload-time = "2026-05-20T11:54:50.584Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/a528eb09d8be51954c485864bd06753e616939a080cbc3dd4417e8c94a57/duckdb-1.5.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e75a6122c12579a99848517f6f00a4e342aebda3590c30fe9b5cc5f39d5e6afc", size = 32626254, upload-time = "2026-05-20T11:54:53.65Z" }, - { url = "https://files.pythonhosted.org/packages/ec/3c/1534c0a6db347c05eb7d0f6ecfb7aefbe74cbff398e4892a8fd1903a20e8/duckdb-1.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fd3963c1cb9d9567777f4a898a9dbe388a2fe9724681801b1e7d6d93eecf1b76", size = 17300917, upload-time = "2026-05-20T11:54:56.628Z" }, - { url = "https://files.pythonhosted.org/packages/23/fa/beafb91e6e152d2161c4a9cbc472334c87607eb61ad7104b5a7fa8d8d7b1/duckdb-1.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3d5db8c0b55e072cf437948ebb5d7e23d7b9d03d905fa5f9145583e65aa447f7", size = 15449411, upload-time = "2026-05-20T11:54:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/50/0a/49b6fe04e2fcd63729eb607dadd44818dde77342a4f5ce086c6c92f1dd4d/duckdb-1.5.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ce80aed7a538422129a57eaca9141e3afb51f8bf562b1908b1576c9725b5b22", size = 19333120, upload-time = "2026-05-20T11:55:01.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/4c/0907c3f76adb9dd90e67610b31e0304a35814e65c4c41a354a262c09b885/duckdb-1.5.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787df63824f07bf18022dbc3b8ca4b2bfab0ebe616464f55c6e8cd0f59ea762e", size = 21453266, upload-time = "2026-05-20T11:55:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9c/d2f23a7803ddbbd9413f7572ecf66a15120ed5ced7ce5c73e698c1406b76/duckdb-1.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:bb5bb5dcdd09d62ee60f0ddbbef918e71cce304ffe28428b1131949d39ffaabf", size = 13118640, upload-time = "2026-05-20T11:55:07.389Z" }, - { url = "https://files.pythonhosted.org/packages/27/d5/7ba2316415bcdab6edd765bbbe35c2ca8a3800f2fe695cd70e3cdb997f09/duckdb-1.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:2fa17ecdd5d3db122836cb71bb93601c2106a3be883c17dffddc02fbf3fa7888", size = 13926409, upload-time = "2026-05-20T11:55:10.166Z" }, - { url = "https://files.pythonhosted.org/packages/a5/c2/d4b6f8a5e4d3bc25773be6da76a99d9661ebbf3552c007c460d2dd59dbf8/duckdb-1.5.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4bfa9a4dadf71e83e2c4eaca2f9421c82a54defecc1b0b4c0be95e2389dec4fe", size = 32636685, upload-time = "2026-05-20T11:55:13.158Z" }, - { url = "https://files.pythonhosted.org/packages/42/58/e835c8298979d29db7a62cb5acc29e9b57aeaca7cdde2fcd3ac980f5cb18/duckdb-1.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aea7baf67ad7e1829ac76f67d7dcbd7fb1f57c3eb179d55ac30952df4709ae30", size = 17308134, upload-time = "2026-05-20T11:55:16.194Z" }, - { url = "https://files.pythonhosted.org/packages/c9/46/617b51363f5613418c8b224b3cce16b58e6dde80904566bec232579c1d4e/duckdb-1.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b0b4f088a65d77e1217ce5d7eff889e63fedc44281200d899ff47c84d8ff836", size = 15449891, upload-time = "2026-05-20T11:55:18.687Z" }, - { url = "https://files.pythonhosted.org/packages/b3/72/354146656e8d9ba3853d3a5ee80a481b8c5f70edfc3d5ae80a8c4479c967/duckdb-1.5.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe8d0c1f6a120aa03fa6e0d03897c71a1842e6cf7afd31d181348391f7108fe1", size = 19338499, upload-time = "2026-05-20T11:55:21.34Z" }, - { url = "https://files.pythonhosted.org/packages/56/8f/65fc623b51448f2bfba1a9ec6ab3debb4664c0876c0113a5e782600b53ac/duckdb-1.5.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0405eae18ec6e8210a471c97dbfe87a7e4d605274b7fe572a1f276e92158f13", size = 21455828, upload-time = "2026-05-20T11:55:23.847Z" }, - { url = "https://files.pythonhosted.org/packages/2b/db/d0274cbe9f5fe219f77c0bdf900ac77103569e83c102a4225ce04cbc607d/duckdb-1.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:33ae08b3e818d7613d8936744b67718c2062c2f530376895bfd89efb51b81538", size = 13640011, upload-time = "2026-05-20T11:55:26.276Z" }, - { url = "https://files.pythonhosted.org/packages/07/5d/8f1899b8bef291caf953992fcd6c24df9f29387a35645e58c2504a5ca473/duckdb-1.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:746433e49bbc667b4df283153415fbe37e9083e0eff6c3cd6e54de7536869cd4", size = 14411554, upload-time = "2026-05-20T11:55:29.037Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ee/69340af74a3aa21838f14c16a0dd3e58461896ccba41f6bc7f0a01536e23/duckdb-1.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ddd9533ce80f9b851bdd6276960a9286166514a9ceca43d5bc2f0d5842c490d", size = 32656177, upload-time = "2026-06-17T10:47:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/9da267ade389d4e7e533ac0c77b3a7041513a66efab93beb84f27627b0b8/duckdb-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:360f2542d09759c3739400f8b787e29b43ba0da665c21756216291458bf6fc59", size = 17318966, upload-time = "2026-06-17T10:47:33.688Z" }, + { url = "https://files.pythonhosted.org/packages/76/b4/ad73c1a396288e443b18af50819448060b318c1e933305167c1d7f98a507/duckdb-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0cf932055061e544d3fa27cc6c147da25f3f681ee5980157fb55e77d6c2d9c63", size = 15467572, upload-time = "2026-06-17T10:47:36.071Z" }, + { url = "https://files.pythonhosted.org/packages/6f/06/2c52ce3b97c3f21111f3c98a2121ed002e33f86488f55098a37825af6d4a/duckdb-1.5.4-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2d58d39f5e65419cdc27e3875cba4a729a3bbf6bf4016aefb4a2a65335a1d42", size = 19344044, upload-time = "2026-06-17T10:47:38.174Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7d/5c0cc66fb90a1b14474eebb5ab535eacc51cb20b0e45358348b51c07abc9/duckdb-1.5.4-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9a10dc40469b9c0e458625d2a8359461a982c6151bb53ff259fea00c4695ad4", size = 21448215, upload-time = "2026-06-17T10:47:40.617Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2c/16c3ea201855cdfb7dde52ea3678d0536861cb485ffad46cf345436d658d/duckdb-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:3565550adbf160ef7a2ee3395470570182f11233983ad818bd7d5f9e349f92b2", size = 13132138, upload-time = "2026-06-17T10:47:43.085Z" }, + { url = "https://files.pythonhosted.org/packages/56/bb/7921dabd50daef3969f14cd8a5a14c24eee337db7914a462f2defa8add92/duckdb-1.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3fb41d9cfccb7e44511eeeed263ae98143ca63bdb1ef84631ba637c314efa1b5", size = 32663142, upload-time = "2026-06-17T10:47:45.471Z" }, + { url = "https://files.pythonhosted.org/packages/a6/83/2137765eaba6a9aefe3bb9848ddaac7407fe3ba19b292f98b31f3b7ab27f/duckdb-1.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ba7b666bc9c78d6a930ee9f469024149f0c6a23fb7d2c3418aad6774339bec0", size = 17321485, upload-time = "2026-06-17T10:47:47.778Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b2/a02c1ee43fd7e8cf1fc2e3d377f3dcf9d4a3e58a4549557516e1866ff0da/duckdb-1.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9d9e6817fcbc09d2605a2c8c041ac7824d738d917c35a4d427e977647e1d7944", size = 15470820, upload-time = "2026-06-17T10:47:49.977Z" }, + { url = "https://files.pythonhosted.org/packages/d8/48/a243d30223b024bc6057abe472b002cff01e97efefb4d2f0b0dcc5aece0b/duckdb-1.5.4-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02dd9f9a6124069213f13e3a474c208028c472fe1acdae12b38761f954fe4fc6", size = 19341849, upload-time = "2026-06-17T10:47:52.205Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/a5d48de4771e2403a8ef26a20dc7457b1c8f7e398ff0caf9c0cad8805f89/duckdb-1.5.4-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccc7f2694d02b4763fee61021d45e12f7bc5743993686563957df0cef799fbae", size = 21451698, upload-time = "2026-06-17T10:47:54.653Z" }, + { url = "https://files.pythonhosted.org/packages/79/b8/8244d7741b4afae67775cf0cb0d4eb9e923a83110907e4801e17fa078480/duckdb-1.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:4c430e788d99b50854209bf2833ba36a45df75e57f86efb477046cd408bbd077", size = 13132643, upload-time = "2026-06-17T10:47:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/e4/57/8169822a37f6dd7d561c567f9007e3cf04bf97bccb619afe90db849c0962/duckdb-1.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:e2dc8340cfb6006025a798c50f40126d6e945a1d2487be94667bb4166556ce7b", size = 13986386, upload-time = "2026-06-17T10:47:59.345Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f2/e2f4b477ae3a3b40e8b5f429832e48edb62ed9da99807cc4902e157e5646/duckdb-1.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:291a9e7502551170af989ff63139a7a49e99d68edbc5ef5017ac27541fe54c65", size = 32708876, upload-time = "2026-06-17T10:48:01.527Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2b/b698d82a5e1e30b6a05748d72045f672994c6b22f4f0f8423523608b991f/duckdb-1.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83e8c089bbb756ca4471d8b05943b80a106058697cf00615e70423106bb783bc", size = 17346125, upload-time = "2026-06-17T10:48:04.035Z" }, + { url = "https://files.pythonhosted.org/packages/71/75/37e13f39268eaf34864453b3a039c4a1ff0b088d3eae45a4289b41c98c1b/duckdb-1.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ff96d2a342b200e1ec6f1f19986c77f4ac16a49b6112f71c5b763989203a9d60", size = 15488133, upload-time = "2026-06-17T10:48:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/cc/59/2d082af578f689231798245b54562c61416e49049b0bda81a06c56a4b53e/duckdb-1.5.4-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f935ef210ab00bc94bb1e3052697adaa36bb0ce7bdfeda8b0f34e2ff1643870", size = 19367895, upload-time = "2026-06-17T10:48:08.59Z" }, + { url = "https://files.pythonhosted.org/packages/52/2b/55c34d2863a76ca824ef8274691e84240b4ff1acde3d231709e82557c240/duckdb-1.5.4-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cda263d8c20addb8d4f95464787cbe0af1144f7ab7e21db3709fb826ee01725", size = 21486499, upload-time = "2026-06-17T10:48:10.963Z" }, + { url = "https://files.pythonhosted.org/packages/cf/30/ade5952b8182fac86fab43b95ebe3836e66381d0ad64eb1e54bd8207c988/duckdb-1.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:266c7c909558ce7377f57d082cee408aadebdd9111be017558ca54e44a031037", size = 13147934, upload-time = "2026-06-17T10:48:13.061Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/278f0f70e25b9911afe2fd227b9460f2e6d76177f0dcc03f7f1454afefa5/duckdb-1.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:f14e79a006341f29ee5a2692a24dac5114e77533d579c57ec39124adf0135033", size = 13965235, upload-time = "2026-06-17T10:48:15.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/3fcb34e523a9bad1f0557a6c7691a71ba66c43a05e5be9ee96a9a841ed65/duckdb-1.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:42a612e67d64450b446eb69695290d460713eef46e0f64467ab9dfe96264ee05", size = 32708366, upload-time = "2026-06-17T10:48:18.084Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/bff5054c2c1d65decab36aa6296621e51a2a575a9f250db7ab9b83a325d6/duckdb-1.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3fb6f07d54ecf4d0d3c5179a2361fdddfafa14de4fc42696de4632479b703421", size = 17345735, upload-time = "2026-06-17T10:48:20.67Z" }, + { url = "https://files.pythonhosted.org/packages/93/12/d1b2b344e9699246aada6f9de5156e708fb476e2780e5bff9b5d95fe11d9/duckdb-1.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f32ad7e0286c1c29ab6c73b29118c86101f8eee46aae54f54d0b50916f542f6", size = 15488568, upload-time = "2026-06-17T10:48:23.038Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d1/ac56c6096e3e95da60b2c5dd5a0f0eb5540a80622e2e4f8faab893ec4e96/duckdb-1.5.4-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:698ec90bd5d5538bd5f6d212a4b61af443d240703cf45f134738535026556ea5", size = 19368184, upload-time = "2026-06-17T10:48:25.601Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/2ae4c3e157a19d9b4ac1f09a5dea6f93012334cc2db09f1e0c71eb99693d/duckdb-1.5.4-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136cea7f886b78caf4035485b4b1e766e8b309e999f9e83a966f81ebb8122844", size = 21486523, upload-time = "2026-06-17T10:48:27.817Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/c3d8d21e0d0db8faa81eeeb3a55b9932f5a0a16466cb968dc713a653d701/duckdb-1.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:bd6777e8ddd74fb603a6d09766bfcff28638189f8aaa61fc0dffd9e9a4baa8e5", size = 13147807, upload-time = "2026-06-17T10:48:30.017Z" }, + { url = "https://files.pythonhosted.org/packages/44/48/ddf8d3740e3d28582944f70d84e720b5dc28c10ec22b668a0e0bd965f2f2/duckdb-1.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:73f4878a3012283024a64a1909e440aac12091ef336f671fc142f7e87449ce0c", size = 13965189, upload-time = "2026-06-17T10:48:32.251Z" }, + { url = "https://files.pythonhosted.org/packages/62/01/67ac4cbc8e552a1e14c029b5c443d828e68f94d5d913c574f577e1db277e/duckdb-1.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4647968629d0677bbcc2416c7aeda8685eb84e4ca15a6dbd4f82a66cfc91a532", size = 32714364, upload-time = "2026-06-17T10:48:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/eb44d983fa56b175f971eea251bde284a36d26cbb93fcb68035061f54078/duckdb-1.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e8fcef301cf68d3951ea1eb8ac4d76cea0a6f6a08f4c78fe4026fc96d217bebc", size = 17349820, upload-time = "2026-06-17T10:48:37.126Z" }, + { url = "https://files.pythonhosted.org/packages/10/b2/b9dc7624b105d414585b8530451c1162c0b4750c0be9be2e497bb47a8a9b/duckdb-1.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f6f39cd0dc6948dee17fd130aec55114f97a8ef6e1db519b9774087962bc5c8c", size = 15498160, upload-time = "2026-06-17T10:48:40.032Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/61356444f6a8c62dec3c3d129abfc53f428de1d484093d1bb381db441231/duckdb-1.5.4-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:262f068158beb5943f2c618f4e54b46db8306b959f90dce956f90a89f613673d", size = 19374183, upload-time = "2026-06-17T10:48:42.698Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f4/d5d633dd7c5138d8f7c434e6ac2553c831b7fb658494efa8d0bc73df8623/duckdb-1.5.4-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d2307a76d199077b0055b354e90e857479461a0d875437535dd4833172c8b6d", size = 21487202, upload-time = "2026-06-17T10:48:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/c0/26/5be13bbd5c3421dccfc1ad4ca9da4b97c5a3ddd73f66542092f3167ec52c/duckdb-1.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:6dcbb81a1276bc48deb4d562bce4f8895e4fc6348750a096e30052345c6d6552", size = 13666989, upload-time = "2026-06-17T10:48:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/dc/82/4d52f3f9f9703a226b26b80bdae3f6905aeefe5221bf1815fc93ff02ca25/duckdb-1.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:0f8722346024e5d9f02b58bf7b0491a629f97fdc8a04a10e432940f471ee387a", size = 14449863, upload-time = "2026-06-17T10:48:50.18Z" }, ] [[package]] @@ -1092,11 +1102,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.29.1" +version = "3.29.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/f9/f38573ed5844586db374d085911740a501ccfa373b455fc9413f09f85237/filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e", size = 59335, upload-time = "2026-06-03T15:19:04.053Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/c8/35bdf04fb30755e2ed758f877edf3eb4a243c2463d3a258cc28b18b7a6e2/filelock-3.29.6.tar.gz", hash = "sha256:895c532ef3f4ef04972b9446a8c4e2931a5c399ff3c4be4c9369f2639b80f793", size = 70301, upload-time = "2026-07-06T23:08:08.577Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/7467c2946ccd9617f7da38187071bdc45bb9a95df51f4d63d6622432ce4e/filelock-3.29.6-py3-none-any.whl", hash = "sha256:14d5f5597d2e0c4dbd774cfb6d8132da1db44da83732aab679d54f7dcf97ab65", size = 45478, upload-time = "2026-07-06T23:08:07.197Z" }, ] [[package]] @@ -1222,11 +1232,11 @@ wheels = [ [[package]] name = "fsspec" -version = "2026.4.0" +version = "2026.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, ] [[package]] @@ -1255,15 +1265,15 @@ wheels = [ [[package]] name = "google-auth" -version = "2.53.0" +version = "2.55.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/f3f4ac177c67bbee8fe8e88f2ab4f36af88c44a096e165c5217accf6e5d3/google_auth-2.55.1.tar.gz", hash = "sha256:fb2d9b730f2c9b8d326ec8d7222f21aef2ead15bf0513793d6442485d87af0a1", size = 349527, upload-time = "2026-06-25T23:39:27.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl", hash = "sha256:eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995", size = 252349, upload-time = "2026-06-25T23:38:52.946Z" }, ] [package.optional-dependencies] @@ -1273,7 +1283,7 @@ requests = [ [[package]] name = "google-genai" -version = "2.7.0" +version = "2.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1287,18 +1297,18 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/7b/6eb3b3d545b6bb4c374acba1ccf91b0f33b605e551536a6243cfcef2f07f/google_genai-2.7.0.tar.gz", hash = "sha256:3c6f32f5ced9877ededd1b384b5e5b7f09c20046ec3390b662b16d8cd1882ac5", size = 555853, upload-time = "2026-05-28T15:39:24.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/fe/b796087493c3c55371aa58b9f264841ace5bfdf8c668cafa7afa33c44bec/google_genai-2.10.0.tar.gz", hash = "sha256:77912cd558cd7dfd5b75c25fd1c609e78d7954dde583331104022a46ea90f9ee", size = 600039, upload-time = "2026-06-24T01:33:18.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/dd/7a8be39e9d698e80e9db796514efbc6083dbd787bdb9a101e8ba47248e5e/google_genai-2.7.0-py3-none-any.whl", hash = "sha256:21cac381e09a869151706aba797b6a4f96cfe92c484e13204d092caee7ff11cb", size = 822545, upload-time = "2026-05-28T15:39:22.907Z" }, + { url = "https://files.pythonhosted.org/packages/e0/39/00bcfd94de255d24249401efff4f48d77bf6066b46447e519fa193c0c299/google_genai-2.10.0-py3-none-any.whl", hash = "sha256:d5350311567ae660c24cbc1752aee4b3d660f89c0106d2dcd2a69978c35afe1e", size = 957974, upload-time = "2026-06-24T01:33:16.296Z" }, ] [[package]] name = "griffelib" -version = "2.0.2" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, ] [[package]] @@ -1312,34 +1322,34 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.5.0" +version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, - { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, - { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, - { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, - { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, - { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, - { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, - { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, - { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, - { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, - { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, - { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, - { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, - { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, - { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, - { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, + { url = "https://files.pythonhosted.org/packages/64/ee/dd9ba7beae1005e54131b7d45263cc74c8a066d47d354e6d58ae9445a388/hf_xet-1.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:dbf48c0d02cf0b2e568944330c60d9120c272dabe013bd892d48e25bc6797577", size = 4069485, upload-time = "2026-06-08T23:02:13.193Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/9cae6cfeb4e03070874e73e5c97c66eb90369d3206b6a2b1ef5f96520888/hf_xet-1.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78e4e5192ad2b674c2e1160b651cb9134db974f8ae1835bdfbfb0166b894a43", size = 3838493, upload-time = "2026-06-08T23:02:15.282Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b4/d5c01e0eb6d9f2ca2dacd84d0d1b71e6cfbb2ef3208c968528e010e9b3d7/hf_xet-1.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f7a04a8ad962422e225bc49fbbac99dc1806764b1f3e54dbd154bffa7593947", size = 4505658, upload-time = "2026-06-08T23:02:17.196Z" }, + { url = "https://files.pythonhosted.org/packages/76/c5/29a7598c0c6383c523dc22186d577f4e04267a626cd95ae60f67c00bfe66/hf_xet-1.5.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d48199c2bf4f8df0adc55d31d1368b6ec0e4d4f45bc86b08038089c23db0bed8", size = 4292822, upload-time = "2026-06-08T23:02:18.608Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/dceaf6ca69390126b86ea825fb354b93d01163199070b7bd849225de9468/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:97f212a88d14bbf573619a74b7fecb238de77d08fc702e54dec6f78276ca3283", size = 4491255, upload-time = "2026-06-08T23:02:20.124Z" }, + { url = "https://files.pythonhosted.org/packages/48/a7/e5a7afaacf6c1791fdbeeac42951fb81c3d2bc482992b115dedcc86d963e/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f61e3665892a6c8c5e765395838b8ddf36185da835253d4bc4509a81e49fb342", size = 4711062, upload-time = "2026-06-08T23:02:21.863Z" }, + { url = "https://files.pythonhosted.org/packages/53/49/2802f8433c9742ce281bddc1e65c02c32268ca3098d66828b05e12e45ee2/hf_xet-1.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f4ad3ebd4c32dd2b27099d69dc7b2df821e30767e46fb6ee6a0713778243b8ff", size = 4017205, upload-time = "2026-06-08T23:02:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5a/50c71195b9fb883659f596e7252faf4c18c58e753a9013bdbf9bac5d2250/hf_xet-1.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:8298485c1e36e7e67cbd01eeb1376619b7af43d4f1ec245caae306f890a8a32d", size = 3845426, upload-time = "2026-06-08T23:02:25.124Z" }, + { url = "https://files.pythonhosted.org/packages/05/24/5e0c28f80371c17d49fed004597d9d132cb75c1f6f53db2cb95f459d2312/hf_xet-1.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3474760d10e3bb6f92ff3f024fcb00c0b3e4001e9b035c7483e49a5dd17aa70f", size = 4069676, upload-time = "2026-06-08T23:02:26.759Z" }, + { url = "https://files.pythonhosted.org/packages/d2/17/261ba565b6a4d960fb478f61fdf919c0be5824645aaf1c319eca660c1611/hf_xet-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6762d89b9e3267dfd502b29b2a327b4525f33b17e7b509a78d94e2151a30ce30", size = 3838509, upload-time = "2026-06-08T23:02:28.573Z" }, + { url = "https://files.pythonhosted.org/packages/4e/44/7ffdc2e184b0d41fc0f683ba3936ef669ab63cf242cf36ef50e57d683668/hf_xet-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf67e6ed10260cef62e852789dc91ebb03f382d5bdc4b1dbeb64763ea275e7d6", size = 4505881, upload-time = "2026-06-08T23:02:30.257Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/788060d5aa4d5e671f1a31bf69624c314eb2d8babab3aa562f9e5d53444e/hf_xet-1.5.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c6b6cd08ca095058780b50b8ce4d6cbf6787bcf27841705d58a9d32246e3e47a", size = 4292995, upload-time = "2026-06-08T23:02:31.993Z" }, + { url = "https://files.pythonhosted.org/packages/22/93/c5540cbd6b55529b7dc42f6734e88cebee21aefbea34128b66229df56c57/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1af0de8ca6f190d4294a28b88023db64a1e2d1d719cab044baf75bec569e7a9", size = 4491570, upload-time = "2026-06-08T23:02:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/03/f3/9d8ceab30f44f36c1679b1b8683054c71a0dadc787dbf07421891742d3ca/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4f561cbbb92f80960772059864b7fb07eae879adde1b2e781ec6f86f6ac26c59", size = 4711565, upload-time = "2026-06-08T23:02:35.454Z" }, + { url = "https://files.pythonhosted.org/packages/cd/54/27ed9a5e2cc583b4df82f75a03a4df8dbf55f5a9fa1f47f1fadfb20dbeac/hf_xet-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e7dbb40617410f432182d918e37c12303fe6700fd6aa6c5964e30a535a4461d6", size = 4017343, upload-time = "2026-06-08T23:02:37.14Z" }, + { url = "https://files.pythonhosted.org/packages/ae/12/ecb2fc8d45e767580e3a37faa97cb895608b614965567efb4f18cff67e27/hf_xet-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6071d5ccb4d8d2cbd5fea5cc798da4f0ba3f44e25369591c4e89a4987050e61d", size = 3845716, upload-time = "2026-06-08T23:02:39.073Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" }, ] [[package]] @@ -1372,7 +1382,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.17.0" +version = "1.22.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1383,12 +1393,11 @@ dependencies = [ { name = "packaging" }, { name = "pyyaml" }, { name = "tqdm" }, - { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/65/9826515abb600b5722bcf53f8b4a2fb58340b1f8bfcaee19f83561c13a44/huggingface_hub-1.17.0.tar.gz", hash = "sha256:fad842b6763ef70ebc3919665b1b9273645203185400a7d6c5eddc2323cc3435", size = 797082, upload-time = "2026-05-28T15:12:13.347Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/ea/dc54b4dda5841cb3a7812a178695be776e7c15c597887c2ed892f17d015a/huggingface_hub-1.22.0.tar.gz", hash = "sha256:e2dfe5fe1ec3b87ba2709aa34555b23e3f3f6ad4d7255238e13ddb8348e6bbfa", size = 914232, upload-time = "2026-07-03T09:46:44.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/28/d7cef5e477b855c25d415b8f57e5bc7347c7a90cad3acf1725d0c92ca294/huggingface_hub-1.17.0-py3-none-any.whl", hash = "sha256:3b8156d23118e87f6a587648bfbc04f04a12a757ccb4ed298b35c4ae638bf24c", size = 671546, upload-time = "2026-05-28T15:12:11.441Z" }, + { url = "https://files.pythonhosted.org/packages/64/9c/a1a377265abd8b823a2c661c665028ccb6b9fba1ca9d08e52ff679c20ecd/huggingface_hub-1.22.0-py3-none-any.whl", hash = "sha256:b09e19309ae09ee0a71892701c4fe70af39ab4e00817321dc62f2289a977249b", size = 765085, upload-time = "2026-07-03T09:46:42.832Z" }, ] [[package]] @@ -1449,7 +1458,7 @@ wheels = [ [[package]] name = "ibm-watsonx-ai" -version = "1.5.13" +version = "1.5.14" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -1471,9 +1480,9 @@ dependencies = [ { name = "tabulate", marker = "python_full_version >= '3.11'" }, { name = "urllib3", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/8c/15c3344d064e3267e31561e1ad2df0f8f0a01c5f14a2ed2436d0bd7ce445/ibm_watsonx_ai-1.5.13.tar.gz", hash = "sha256:63a38fa0ea5ac604e703e3d76888742528d046f376cbaa16762b4b98b0cec973", size = 734189, upload-time = "2026-06-03T08:38:00.984Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/a3/c756b534696ab2f3f29882fdb7ca7198b7a5c94e10c0a3a327853d6d6b79/ibm_watsonx_ai-1.5.14.tar.gz", hash = "sha256:a756488bd57e87c0fc51be42dcba871143cfe0ac1e805c497c5047e1e4f13e9d", size = 735804, upload-time = "2026-06-22T12:32:43.85Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/9c/b295dd5cc0a414c627ec3231b9239a9e51077a4c3942debd0ef0e30056f7/ibm_watsonx_ai-1.5.13-py3-none-any.whl", hash = "sha256:43212a8467c565e87913a96da7f71e7cba9ff47f1df1b02e77a5a57898da7911", size = 1113137, upload-time = "2026-06-03T08:37:59.309Z" }, + { url = "https://files.pythonhosted.org/packages/84/bf/42a3bc150db219092ebd56f5ba79d5351bb16072d5603c462c63a2421b9c/ibm_watsonx_ai-1.5.14-py3-none-any.whl", hash = "sha256:ec505ad6e4ecb7e6813b453c2f4a317e429488a46ad88409cdce178aa46a1511", size = 1119859, upload-time = "2026-06-22T12:32:42.134Z" }, ] [[package]] @@ -1514,27 +1523,27 @@ wheels = [ [[package]] name = "ipykernel" -version = "7.2.0" +version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "9.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, - { name = "nest-asyncio" }, + { name = "nest-asyncio2" }, { name = "packaging" }, { name = "psutil" }, { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583, upload-time = "2026-06-10T08:41:23.648Z" }, ] [[package]] @@ -1564,7 +1573,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.14.0" +version = "9.15.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -1581,15 +1590,15 @@ dependencies = [ { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, - { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten'" }, + { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, { name = "pygments", marker = "python_full_version >= '3.11'" }, { name = "stack-data", marker = "python_full_version >= '3.11'" }, { name = "traitlets", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/c2/c0064cf15d026501a1ef70e42efd9c3f818663089399aacc5e37a82901c1/ipython-9.14.0.tar.gz", hash = "sha256:6f27ff0f1d9ea050e0551f71568bc4b34d8aba579e8f111c5b4175f44ac6b4aa", size = 4432601, upload-time = "2026-05-29T15:13:24.611Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/a3/9e59340f02c1dc8f8c0a05b09244712b8609eb5439f9996e887e2b82f452/ipython-9.14.0-py3-none-any.whl", hash = "sha256:8fd984a3372c14b12790b084ba6b5cff5678c0cb063244a0034f06a51f20d6c2", size = 627457, upload-time = "2026-05-29T15:13:22.942Z" }, + { url = "https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" }, ] [[package]] @@ -1639,105 +1648,101 @@ wheels = [ [[package]] name = "jiter" -version = "0.15.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/da/76a2c7e510ba15fe323d9509c223ab272da79ea59f54488f4a78da6426db/jiter-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:edebcf7d1f601199084bb6e844d7dc67e03e04f6ac786b0332d616635c4ff7a4", size = 310849, upload-time = "2026-05-19T10:06:51.944Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8e/827be942883a4dc0862c48626ff41af3320b1902d136a0bf4b9041f2c567/jiter-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f924585cdacf631cd382b657966847bb537bf9ed0a6f9b991da5f05a631480f", size = 314991, upload-time = "2026-05-19T10:06:53.522Z" }, - { url = "https://files.pythonhosted.org/packages/6d/38/be2832be361ba1b9517c76f46d30b64e985be1dd43c974f4c3a4b1844436/jiter-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abbf258599526ad0326fe51e252e24f2bd6f24f1852681b4b78feda3808f1d18", size = 340843, upload-time = "2026-05-19T10:06:55.071Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d8/90f01fb83c0c7ba509303ec93e32a308fbfa167d264860b01c0fd0dbbd06/jiter-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c468136b8bd6bb18c8786e4236a1fa27362f24cb23450ba0cb204ab379b8e6f", size = 365116, upload-time = "2026-05-19T10:06:56.893Z" }, - { url = "https://files.pythonhosted.org/packages/91/38/94593d34f8c67a0b6f6cbc027f016ffa9780b3a858a7a86f6fd7a15bcc1e/jiter-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05906b93d72f03339e6bb7cf8dc10ebda64a0266126eed6beba79e20abcf5fd4", size = 457970, upload-time = "2026-05-19T10:06:58.707Z" }, - { url = "https://files.pythonhosted.org/packages/df/04/d79962dd49d00c97e2a9b4cacea1947904d02135936960351f9a96d4c1a6/jiter-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30ce785d2adb8e32c3f7741442370a74834ec4c01f3c48f0750227a0b4ef27d6", size = 375744, upload-time = "2026-05-19T10:07:00.471Z" }, - { url = "https://files.pythonhosted.org/packages/c3/2e/5d37abe2be0e819c21e2338bebd410e481763ce526a9138c8c3652fa0123/jiter-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd73e3da91a0a722d67165e849ce2cdc10de0e0d48738c142be8c6c5f310f4c", size = 349609, upload-time = "2026-05-19T10:07:01.829Z" }, - { url = "https://files.pythonhosted.org/packages/7a/90/98768ad2ed90c1fda15d64157de2dfbf73c1c074d4b1bfaca915480bc7cf/jiter-0.15.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:ceb8fc27d38793f9c97149be8302720c5b22e5c195a37bf2c45dc36c4600a512", size = 354366, upload-time = "2026-05-19T10:07:03.587Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c4/fbfb806209f1fe4b7dccdfb07bc62bb044300734a945b06fd64db446ef6a/jiter-0.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d726e3ceeb337191324b49de298142f27c3ad10886341555d1d5315b5f252c6a", size = 393519, upload-time = "2026-05-19T10:07:05.08Z" }, - { url = "https://files.pythonhosted.org/packages/37/1c/b9c257cd70cb453b6d10f3ebf0402cdb11669ab455389096f09839670290/jiter-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c8aea7781d2a372227871de4e1a1332aa96f5a89fd76c5e835dafdbad102887", size = 519952, upload-time = "2026-05-19T10:07:06.589Z" }, - { url = "https://files.pythonhosted.org/packages/a9/1a/aa85027db7ab15829c12feebbc33b404f53fc399bd559d85fd0d6365ff0d/jiter-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf4bd113a69c0a740e27cb962ce10630c36d2b8f59d759a651b955ee9d18a823", size = 550770, upload-time = "2026-05-19T10:07:08.228Z" }, - { url = "https://files.pythonhosted.org/packages/d4/54/8c3f65c8a5687925e84708f19d63f7f37d28e2b86a48d951702ad94424d8/jiter-0.15.0-cp310-cp310-win32.whl", hash = "sha256:d92a5cd21fdb083931d546c207aa29633787c5dc5b02daab2d32b843f88a2c53", size = 209303, upload-time = "2026-05-19T10:07:10.006Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/0528a1eb9f42dd2d8228a0711458628f35924d131f623eaebc35fd23d3d4/jiter-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:e58585a58209d72691ce2d62a9147445f5a87beb0bde97fde284c96ae392a3d1", size = 200404, upload-time = "2026-05-19T10:07:11.426Z" }, - { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" }, - { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" }, - { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" }, - { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" }, - { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" }, - { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" }, - { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" }, - { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" }, - { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, - { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, - { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, - { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, - { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, - { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, - { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, - { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, - { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, - { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, - { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, - { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, - { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, - { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, - { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, - { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, - { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, - { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, - { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, - { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, - { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, - { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, - { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, - { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, - { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, - { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, - { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, - { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, - { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, - { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, - { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, - { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, - { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, - { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, - { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, - { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, - { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, - { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, - { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, - { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, - { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" }, - { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" }, - { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" }, - { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, - { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, - { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, + { url = "https://files.pythonhosted.org/packages/76/d8/b959609e44012a42b1f3e5ba98ea3b33c7e41e6d4b77cd8f00fd19b1d3ad/jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c", size = 310082, upload-time = "2026-06-29T13:02:31.356Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/4d7f5667ea0e0548534ba880b84bb3d12924fd133aa83ad6c6c80fca3d76/jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b", size = 315643, upload-time = "2026-06-29T13:02:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/9b/83/bed2dcb5c9f3e1ccfcbc67dda48265fe7d5ad0c9cadda5fe95f6e3b87f94/jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84", size = 341363, upload-time = "2026-06-29T13:02:34.853Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2f/6bb3c3dda668ebc0445689c81a2b0f26a82b10843d67ed9c9b2c3edc177f/jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c", size = 365483, upload-time = "2026-06-29T13:02:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/92/35/8a045ccb39164e70dcdae696413b661771f148b68b12b175c3a04d901937/jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126", size = 461219, upload-time = "2026-06-29T13:02:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/e7/99/22292dbbf0ed0c610cfe5ddc7f3bd67237a412f121318f865196e62a07bd/jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c", size = 374905, upload-time = "2026-06-29T13:02:40.357Z" }, + { url = "https://files.pythonhosted.org/packages/29/ac/2f55ccb1f0eeafa6d89d24caf52f6f0944a59290ee199e9ade62177dca42/jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de", size = 348320, upload-time = "2026-06-29T13:02:41.923Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/7d88b9174c40064fabc07c84a9b62e6b10f5644562ec0e0a29392edbe978/jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244", size = 356519, upload-time = "2026-06-29T13:02:43.436Z" }, + { url = "https://files.pythonhosted.org/packages/27/57/c4a33aeef513a9d5e26e31534e0bcc752d6ea0e54c94ddb7b68bade669c2/jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f", size = 394204, upload-time = "2026-06-29T13:02:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c6c23e76ebb3766b111bc399437bbc9f870a76e2a92e10b2a5f561d57372/jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131", size = 521477, upload-time = "2026-06-29T13:02:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d3/0001c8c0c5976af2625bb1cfb1895e8ec693b6589fe4574b8e6fc2c85501/jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b", size = 552187, upload-time = "2026-06-29T13:02:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/f6/76/311b718e07e85740e48619c0632b36f7e0b8d113984499e436452ed13a9a/jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9", size = 206513, upload-time = "2026-06-29T13:02:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/db/7f/ac680eeb0777dc0eb7dc824800ba27880d7f6bc712e362d34ad8ee559f36/jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26", size = 199505, upload-time = "2026-06-29T13:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, ] [[package]] @@ -1767,7 +1772,7 @@ dependencies = [ { name = "jsonschema-specifications" }, { name = "referencing" }, { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ @@ -1788,7 +1793,7 @@ wheels = [ [[package]] name = "jupyter-client" -version = "8.8.0" +version = "8.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, @@ -1796,10 +1801,11 @@ dependencies = [ { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/5512503b088997c2250b8bf18258fba9d9ce5ead641183700960d3c9d342/jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa", size = 359256, upload-time = "2026-06-09T13:15:01.033Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81", size = 109828, upload-time = "2026-06-09T13:14:58.835Z" }, ] [[package]] @@ -1826,7 +1832,7 @@ wheels = [ [[package]] name = "jupytext" -version = "1.19.3" +version = "1.19.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, @@ -1836,9 +1842,9 @@ dependencies = [ { name = "pyyaml" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/2d/15624c3d9440d85a280ff13d2d23afd989802f25470ac59932f4fef6f0c6/jupytext-1.19.3.tar.gz", hash = "sha256:713c3ed4441afe0f31474d28ea2e6b61a268c04c40fd78e5ccfd7f7ac9e9f766", size = 4305350, upload-time = "2026-05-17T09:09:29.294Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/52/e014296ac8f40ca783aeb73dae52e65edbb0eaae0dcdc1ea41bfaa8aebf7/jupytext-1.19.4.tar.gz", hash = "sha256:739bcd4bc12aa4fe298a38017cdb5ae27b08a6ba3a5470728d2fe9e04b155db1", size = 4581977, upload-time = "2026-06-21T21:48:58.32Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/ec/d9be3bd1db141e76b2f525c265f70e66edd30a51a3307d8edf0ef1909c54/jupytext-1.19.3-py3-none-any.whl", hash = "sha256:acf75492f80895ad8e664fd8db1708b617008dd0e71c341a1abc3d0d07310ed0", size = 170579, upload-time = "2026-05-17T09:09:27.478Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e9/e2ae007456069dbe01865c69a4203a7ada6f7e337b78fc2f12e51bd3fae7/jupytext-1.19.4-py3-none-any.whl", hash = "sha256:032d4ef4bd2e96addcac780b9b1d6b5a266ca39beceaaca95bfb4f06e0b77029", size = 170889, upload-time = "2026-06-21T21:48:56.352Z" }, ] [[package]] @@ -1875,7 +1881,8 @@ dependencies = [ { name = "deprecation" }, { name = "lance-namespace" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "overrides", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "pyarrow" }, @@ -1913,7 +1920,7 @@ embeddings = [ { name = "google-genai" }, { name = "huggingface-hub" }, { name = "ibm-watsonx-ai", version = "1.3.42", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ibm-watsonx-ai", version = "1.5.13", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ibm-watsonx-ai", version = "1.5.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "instructorembedding" }, { name = "ollama" }, { name = "open-clip-torch" }, @@ -2181,14 +2188,14 @@ wheels = [ [[package]] name = "mistune" -version = "3.2.1" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/84/620cc3f7e3adf6f5067e10f4dbae71295d8f9e16d5d3f9ef97c40f2f592c/mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28", size = 98003, upload-time = "2026-05-03T14:33:22.312Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/5f/007786743f962224423753b78f7d7acb0f2ade46d1604f2e0fa2bedf9020/mistune-3.3.2.tar.gz", hash = "sha256:e12ee4f1e74336e91aa1141e35f913b337c40bdf7c0cc49f21fb853a27e8b62f", size = 111284, upload-time = "2026-06-23T00:29:28.568Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" }, + { url = "https://files.pythonhosted.org/packages/44/43/894c2cbbcbdf53b57d1257a249811abe2ee9ab7ef76af301b40f1c054533/mistune-3.3.2-py3-none-any.whl", hash = "sha256:a678a56387d487db7368ede4647cb2ba1deff22ce61f92343e4ebe0ddfce4f2d", size = 61554, upload-time = "2026-06-23T00:29:27.088Z" }, ] [[package]] @@ -2310,7 +2317,7 @@ wheels = [ [[package]] name = "mkdocstrings-python" -version = "2.0.3" +version = "2.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffelib" }, @@ -2318,9 +2325,9 @@ dependencies = [ { name = "mkdocstrings" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" }, ] [[package]] @@ -2498,16 +2505,16 @@ wheels = [ [[package]] name = "narwhals" -version = "2.22.0" +version = "2.23.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/1c/c80cb7719721a44846c6301ef118434bae30a423924bfad3a47f16bdc064/narwhals-2.22.0.tar.gz", hash = "sha256:6486282bb7e4b4ab55963efbd8be1451b764cc4874b74d1fd625eba9dc60b86f", size = 417565, upload-time = "2026-06-01T13:34:36.249Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/66ed1fc6e38a0c0f330627ec5c5d597990d6159b6712b82af0ad2c65f06c/narwhals-2.23.0.tar.gz", hash = "sha256:13e7ff5b4bb4a2f77b907c2e4d8a76e273dfc1323a3c997440a2f9fd26aed408", size = 656209, upload-time = "2026-07-01T11:21:53.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b6/e7cdde7b8e90d5dff25b622f95833ef26567ad184c977278b93a1cbd5717/narwhals-2.22.0-py3-none-any.whl", hash = "sha256:1421797ede01789cc1537619dbc3f36f840737240f748fdb24a60a0225fc80be", size = 453815, upload-time = "2026-06-01T13:34:34.127Z" }, + { url = "https://files.pythonhosted.org/packages/f4/4e/afc8c31605cb8be1d3bb4438c4d979daa104dab6306cd2b87abe9c3a7299/narwhals-2.23.0-py3-none-any.whl", hash = "sha256:769e7b9ab102c93d8fa019f6b4cd1a657909b04a20bf6210e5a35aae06814ae9", size = 458938, upload-time = "2026-07-01T11:21:51.677Z" }, ] [[package]] name = "nbclient" -version = "0.10.4" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-client" }, @@ -2515,9 +2522,9 @@ dependencies = [ { name = "nbformat" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, + { url = "https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895", size = 25288, upload-time = "2026-06-05T07:52:40.115Z" }, ] [[package]] @@ -2561,12 +2568,12 @@ wheels = [ ] [[package]] -name = "nest-asyncio" -version = "1.6.0" +name = "nest-asyncio2" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/73/731debf26e27e0a0323d7bda270dc2f634b398e38f040a09da1f4351d0aa/nest_asyncio2-1.7.2.tar.gz", hash = "sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8", size = 14743, upload-time = "2026-02-13T00:34:04.386Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl", hash = "sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01", size = 7843, upload-time = "2026-02-13T00:34:02.691Z" }, ] [[package]] @@ -2676,10 +2683,6 @@ name = "numpy" version = "2.4.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version == '3.11.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } @@ -2757,6 +2760,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, ] +[[package]] +name = "numpy" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, + { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, + { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, + { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, + { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, + { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, + { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, + { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, + { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, +] + [[package]] name = "nvidia-cublas" version = "13.1.0.3" @@ -2940,7 +3000,7 @@ wheels = [ [[package]] name = "openai" -version = "2.40.0" +version = "2.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2952,9 +3012,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/9f/136562ec6c3b1a50fe06eb0bb34ed21f0d7426ec0140e5cc43ac785b69a5/openai-2.40.0.tar.gz", hash = "sha256:9a756f91f274a24ad6026cbcb2042fd356c8d4a10e8f347b08d34465e585f7a2", size = 781177, upload-time = "2026-06-01T21:48:23.878Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/f5/7c7cb955305cb41f7f3c5fd7e0e38bf6bbf2658468863d4b7b868a5cb8df/openai-2.44.0.tar.gz", hash = "sha256:68a5a5ffad82b8ff7d451c437529fb64f7c3b8123aaf0c021966a882d9e3947d", size = 988753, upload-time = "2026-06-24T20:56:02.293Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/46/180e14be801a75bc13f234cb1b594b232adeb9c84e60a9ab1832e8333591/openai-2.40.0-py3-none-any.whl", hash = "sha256:2b205637ff214477f9ce9ab035e9f494db0e3fa8f1e599008953735fbf6ff1ff", size = 1350935, upload-time = "2026-06-01T21:48:21.462Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/561ed79fd94876160018a5e75254cfcb9b0e62d4dded9dcb20072e86d623/openai-2.44.0-py3-none-any.whl", hash = "sha256:0a2a3ab2e29aeda368700f662ff9ba0f9df17ba4c54577a64e08b8115a3cc0ad", size = 1366216, upload-time = "2026-06-24T20:55:58.882Z" }, ] [[package]] @@ -3083,7 +3143,8 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, { name = "python-dateutil", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, { name = "pytz", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, { name = "tzdata", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, @@ -3149,7 +3210,7 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "python-dateutil", marker = "python_full_version >= '3.14'" }, { name = "tzdata", marker = "(python_full_version >= '3.14' and sys_platform == 'emscripten') or (python_full_version >= '3.14' and sys_platform == 'win32')" }, ] @@ -3239,7 +3300,8 @@ dependencies = [ { name = "accelerate" }, { name = "huggingface-hub" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, @@ -3267,100 +3329,96 @@ wheels = [ [[package]] name = "pillow" -version = "12.2.0" +version = "12.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, - { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, - { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, - { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, - { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, + { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, + { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] [[package]] @@ -3655,14 +3713,14 @@ wheels = [ [[package]] name = "pyarrow-stubs" -version = "20.0.0.20251215" +version = "20.0.0.20260625" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyarrow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/16/ca991ada0416dc02c246e4b3c853f35053676eb704c63720ef24547d2d68/pyarrow_stubs-20.0.0.20251215.tar.gz", hash = "sha256:92c1fda4998f0c13e608d8abc7e4b8537e3ef108f6bf42c58e5af97e7d143e75", size = 236578, upload-time = "2025-12-15T06:47:46.187Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/7d/a3eadf94131428ec61b44bc40a52acca2b093bf0f6a90a0596c44aa9fe62/pyarrow_stubs-20.0.0.20260625.tar.gz", hash = "sha256:cae81ae0900d0710e5b8a6271fef34ccfacb08663bb5f805d94f129baf13130e", size = 237707, upload-time = "2026-06-25T07:27:47.496Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/7c/a467f91c6c3aa4c34d5a1dafca9ae4db01b757877577d6b0ae04908a0180/pyarrow_stubs-20.0.0.20251215-py3-none-any.whl", hash = "sha256:0634e70388cd23e7c78e2abbb1989822edc34df2d2ff4fd50a2316dd0cdafd9f", size = 235697, upload-time = "2025-12-15T06:47:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/29/65/00bb68d5951a1b758646fd33bca1735aebb6d9ed7184f260e1c1d17e6d97/pyarrow_stubs-20.0.0.20260625-py3-none-any.whl", hash = "sha256:26d8b6c0fa9d61ae49ad70d59988d8b88e2fe022cd782dd3333bcd33e6066486", size = 235701, upload-time = "2026-06-25T07:27:48.793Z" }, ] [[package]] @@ -3854,52 +3912,53 @@ crypto = [ [[package]] name = "pylance" -version = "9.0.0rc1" -source = { registry = "https://pypi.fury.io/lance-format" } +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lance-namespace" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pyarrow" }, ] wheels = [ - { url = "https://pypi.fury.io/lance-format/-/ver_vEHBE/pylance-9.0.0rc1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0b6b02a1808bb3072ee7fe4e36614cae6f86302513e73ec7f55b2234a963b24" }, - { url = "https://pypi.fury.io/lance-format/-/ver_1Jipm4/pylance-9.0.0rc1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30f0ebf0d88034301819eb964f9236ce555aaa58e7ab89c5975a3e2250bbb405" }, - { url = "https://pypi.fury.io/lance-format/-/ver_IvKxo/pylance-9.0.0rc1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44609ea2615ea6e684b85478d1694af2026458f61cf7895ecc75e238bfd17aa8" }, - { url = "https://pypi.fury.io/lance-format/-/ver_2hidj1/pylance-9.0.0rc1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:182167a8dba9eeabffbffd53bd5b8548613d4d459b7cd7b34a840dd00cbb806f" }, - { url = "https://pypi.fury.io/lance-format/-/ver_1dFx3r/pylance-9.0.0rc1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8a63b11e814b7eab758bcaf0d6f97eb05ea86203d9fb0af718c462c24c7d6c9c" }, - { url = "https://pypi.fury.io/lance-format/-/ver_2a8dSh/pylance-9.0.0rc1-cp310-abi3-win_amd64.whl", hash = "sha256:2ff8b953ae2b0550490c1a7efd210aa91bc223d200ffac28849056cfd7436d97" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/2f64921bf346e7075aef24a72595db44821724a3d89a9a92dd24e79632aa/pylance-7.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:98422021975be76e72b1572f41b8c9abb3bee5bdc9bfa5e9ce731110a65ed4d1", size = 62134146, upload-time = "2026-05-27T21:59:37.459Z" }, + { url = "https://files.pythonhosted.org/packages/73/1c/c5a01bee0160b55d9a98895cbd33091d038f0a0995b121ab72e629008d02/pylance-7.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bec86ee5b6fbd8bfc493e653f0a1fba0303cfe5492b9b46fc25ab908edc7183", size = 65373684, upload-time = "2026-05-27T22:04:01.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/1fe8b8f7dbfe734d76af76acc994fc360a0d0c79a4874ef69f5a72a58fe3/pylance-7.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881491432c53184e52f8d1db8d5f872f39a03f36fb104bec77b33d379519d8b5", size = 69458555, upload-time = "2026-05-27T22:16:50.567Z" }, + { url = "https://files.pythonhosted.org/packages/76/f0/dd505cf3fd0226ab9d94759acd713125af1d3bfacfd80bbd52e3b9f89509/pylance-7.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:18453999e7fff4f76b16d6b7882c9df0628bd142ff95e2461bd7dd5ee3fe0af3", size = 65394430, upload-time = "2026-05-27T22:05:30.923Z" }, + { url = "https://files.pythonhosted.org/packages/17/ba/2357b81034f28eb00790e258ed140289a6a887a7468ca9df6349fd186b27/pylance-7.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:04a58051d408c60fe76d41a220dcaf8fea8fb6d1aa0ca78a709b60bc3cc8d19a", size = 69473470, upload-time = "2026-05-27T22:17:18.935Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ec/5c00b6303a67d787f9475141832cbdc513d674ac3dcaeef8a7b169905e65/pylance-7.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:467d4864af047eaab4e1370e2f1e88e2c6f507c079874421116cb41d78bc3629", size = 74792863, upload-time = "2026-05-27T22:19:23.875Z" }, ] [[package]] name = "pymdown-extensions" -version = "10.21.3" +version = "11.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, ] [[package]] name = "pyright" -version = "1.1.410" +version = "1.1.411" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/53/e4d8ea1391bd4355231be6f91bf239479aa0014260ed3fb5526eeb12a1f2/pyright-1.1.410.tar.gz", hash = "sha256:07a073b8ba6749826773c1269773efa11b93440d9a6aa60419d9a3172d6dc488", size = 4062013, upload-time = "2026-06-01T17:35:48.894Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/33/288b5868fa00846dacf249633719d747893e54aebd196b9968ac1878a5d3/pyright-1.1.410-py3-none-any.whl", hash = "sha256:5e961bed37cacf96b3f7cd7b1da39b350a9239aa2e69138d0e88f728cfaf296c", size = 6082448, upload-time = "2026-06-01T17:35:46.387Z" }, + { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, ] [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -3910,9 +3969,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -3955,15 +4014,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.4.0" +version = "1.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/12/38c1a0b1e64806780c9563e3fc9f6e472251839662587cfbe9bfaf2ae10a/python_discovery-1.4.0.tar.gz", hash = "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3", size = 68455, upload-time = "2026-05-28T01:15:37.639Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/26/8b004cc36f430345136f6f00fa1aa9ed596c8ed1e8504625fa79522ff39c/python_discovery-1.4.3.tar.gz", hash = "sha256:ad57d7045a862460d4a235986c33f13ed707d3aeb9153fa47eb7dfd0d4673289", size = 70438, upload-time = "2026-07-03T13:21:51.621Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/8d/3d316429f65029532bb1e28ff77b797d86b5ac3915bb44ca4e19aa283d43/python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", size = 33217, upload-time = "2026-05-28T01:15:36.573Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/9b77ecb4644d1bbea94d29abf78f21c47eca6eb79e9745b702ec0bed2e19/python_discovery-1.4.3-py3-none-any.whl", hash = "sha256:b6e1e4a7d9e3f6948c39746ffe8218225162d738ba39d05ab1d2f6c1cac4878c", size = 33885, upload-time = "2026-07-03T13:21:50.174Z" }, ] [[package]] @@ -4131,7 +4190,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } @@ -4141,123 +4200,123 @@ wheels = [ [[package]] name = "regex" -version = "2026.5.9" +version = "2026.6.28" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/05/e4f219230e11e774a6c9987d2ab0d0c6b8573e13a17e143d0015bee710ef/regex-2026.6.28.tar.gz", hash = "sha256:3cb4b6c5cb3060cc31efdc1fbb27c25fb9b29044afd87e40601a1c4d9db54342", size = 416101, upload-time = "2026-06-28T19:56:55.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44", size = 489438, upload-time = "2026-05-09T23:11:29.374Z" }, - { url = "https://files.pythonhosted.org/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a", size = 291270, upload-time = "2026-05-09T23:11:33.254Z" }, - { url = "https://files.pythonhosted.org/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733", size = 289198, upload-time = "2026-05-09T23:11:35.769Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2", size = 784765, upload-time = "2026-05-09T23:11:37.689Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea", size = 852115, upload-time = "2026-05-09T23:11:39.973Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538", size = 899503, upload-time = "2026-05-09T23:11:42.48Z" }, - { url = "https://files.pythonhosted.org/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2", size = 794093, upload-time = "2026-05-09T23:11:44.681Z" }, - { url = "https://files.pythonhosted.org/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989", size = 786234, upload-time = "2026-05-09T23:11:46.882Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9", size = 769895, upload-time = "2026-05-09T23:11:49.324Z" }, - { url = "https://files.pythonhosted.org/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00", size = 774991, upload-time = "2026-05-09T23:11:51.261Z" }, - { url = "https://files.pythonhosted.org/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808", size = 848790, upload-time = "2026-05-09T23:11:53.232Z" }, - { url = "https://files.pythonhosted.org/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248", size = 757679, upload-time = "2026-05-09T23:11:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6", size = 837116, upload-time = "2026-05-09T23:11:57.934Z" }, - { url = "https://files.pythonhosted.org/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4", size = 782081, upload-time = "2026-05-09T23:11:59.607Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac", size = 266247, upload-time = "2026-05-09T23:12:01.116Z" }, - { url = "https://files.pythonhosted.org/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03", size = 278416, upload-time = "2026-05-09T23:12:03.2Z" }, - { url = "https://files.pythonhosted.org/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b", size = 270413, upload-time = "2026-05-09T23:12:04.649Z" }, - { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, - { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, - { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, - { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, - { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, - { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, - { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, - { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, - { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, - { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, - { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, - { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, - { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, - { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, - { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, - { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, - { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, - { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, - { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, - { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, - { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, - { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, - { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, - { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, - { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, - { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, - { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, - { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, - { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, - { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, - { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, - { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, - { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, - { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, - { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, - { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, - { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, - { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, - { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, - { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, - { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, - { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, - { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, - { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, - { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, - { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, - { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, - { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, - { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, - { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, - { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dc/f7a8c9cf0768f704153d358fae2bc883199bc4ea1e4aa458f1be9d0ef2ce/regex-2026.6.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b83932645630965fd860fdb70ebbf964bf3e8007f08851ea424d01f8d35454a8", size = 489471, upload-time = "2026-06-28T19:53:06.385Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/9786a4a2133e2f1cc5897ed3d2da3da29ff54b775ffa38bc5935fc24be82/regex-2026.6.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e81f1952355042e517dc9861ce65c676e4a098f42402993c40461786d1f794d4", size = 291294, upload-time = "2026-06-28T19:53:09.232Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1f/bfe5b529257f0853aa6b94146e0f6462f4d45aa4f3c05d5a828f415dfd40/regex-2026.6.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2097591101d70bcc108af64c46f6066bb698ee067fec5f75beac0be317639311", size = 289216, upload-time = "2026-06-28T19:53:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/25/56/f615165e90ac5f3b72b249240643439520bbac0ac60a9de06868528eba4c/regex-2026.6.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d7538a614b5842bf53ce329d07b43f97754ca7e6db8d69f347e071bce1c953", size = 784787, upload-time = "2026-06-28T19:53:12.393Z" }, + { url = "https://files.pythonhosted.org/packages/04/94/c9e3ad31b3d5fbe1228fee8319e0c02a5460296624f220d08764547fe6ae/regex-2026.6.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5561e47bbe2b75373b695326507743fcdd4d2cc7f5022312024ccf39fa094e0", size = 852137, upload-time = "2026-06-28T19:53:14.287Z" }, + { url = "https://files.pythonhosted.org/packages/c0/77/d506a428e446466ee298f5425a774737d0671d070425ed794bb3314d60c6/regex-2026.6.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c10f2c5a55ab3dd8318d8ad5f11b530e2691c0edebebde7713066f484902c3fb", size = 899525, upload-time = "2026-06-28T19:53:15.987Z" }, + { url = "https://files.pythonhosted.org/packages/aa/72/becc00d839f19401f10a20168b44711c7b02f7f62bba875b2d8f98417435/regex-2026.6.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23f7e0cc60c72486b42a685f1ff4eec90d50d4fb05e4f9c7d5363b03aa02600d", size = 794116, upload-time = "2026-06-28T19:53:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/fa/11/ea2ca423eeaac2e18077a18b058614e9201f130750df2126d444e39acab2/regex-2026.6.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:76493755f79a88d5ed2c9e63a41d3c05997e0a7ffbe76ed8c4ded8be35b8b14c", size = 786257, upload-time = "2026-06-28T19:53:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9e/f5bf7ecbd14ff2086f015c54dc24fd0d74ba5327fef0de479213f8128615/regex-2026.6.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ff0f41a00f23ea5054acb61901380c41813d813eee3f80f800995710bcc52ecd", size = 769914, upload-time = "2026-06-28T19:53:20.564Z" }, + { url = "https://files.pythonhosted.org/packages/43/04/f9040a5360a06241ba5b7f2e6f1c6184e104a84e6f6522535700e94bf8e2/regex-2026.6.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c60b297292e7e1ef5d02a4759f9e452ee4c8bb95e168d8fd0b5db01bd806f9f", size = 775013, upload-time = "2026-06-28T19:53:22.067Z" }, + { url = "https://files.pythonhosted.org/packages/73/97/4e46f7abf2f864319d2bcac609af3c0532968c66a3364337778fd232b83c/regex-2026.6.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7cf03c87f7b9cbc25a8894cf9be83818406677b6b391b003ec7c884923387b5", size = 848814, upload-time = "2026-06-28T19:53:24.575Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b8/3d1f995727799a1e2e693e397acb7358094606e5591b6b5fd3128d2d1409/regex-2026.6.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:418208ea0af51cfed4f46eb9b1ea7cfc990ca284f0084ecbd951460fb089421e", size = 757702, upload-time = "2026-06-28T19:53:26.215Z" }, + { url = "https://files.pythonhosted.org/packages/20/10/fd5653b8572910a4fe9055f8959b070d7d9443c94ce986529fcdb5fb2a3c/regex-2026.6.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7635fa2cddb917a6bbfac7890602573d2d8c4e470703b0640e6f86a988817ec3", size = 837140, upload-time = "2026-06-28T19:53:27.655Z" }, + { url = "https://files.pythonhosted.org/packages/5d/31/da77e3ef7b594a2aacbd03ce3d0050f33ab3e021df50c6901467c9006511/regex-2026.6.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7bb96c13d6cf5880d31bbef84ca701a64d738aa491c2b79975cc33f8ad00a31e", size = 782105, upload-time = "2026-06-28T19:53:29.375Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/c379001448d0f58b6946f168d4af96ad60a16c1553259c27b0df8701b640/regex-2026.6.28-cp310-cp310-win32.whl", hash = "sha256:56f05194c4843957dd8b3af87eb0c52d8cf0509e7f18e172d727f5f8ff840646", size = 266728, upload-time = "2026-06-28T19:53:31.813Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/cb656529efa87d74cce0d69e606c745537016da3bdfae78f342af2242ee3/regex-2026.6.28-cp310-cp310-win_amd64.whl", hash = "sha256:70710927033af3b54369f17aaba1343b97a23d0b1aa994fa1512b08b1b8c136a", size = 277901, upload-time = "2026-06-28T19:53:33.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ac/d35ccc309c9409406445ab2ef0b56f6a341a916ccff49ff9ac5cc6bb8e9b/regex-2026.6.28-cp310-cp310-win_arm64.whl", hash = "sha256:ed7b30185ee3f8b9b053b0be567b4d226016e2afbebc17fde1c6a4580937b688", size = 276880, upload-time = "2026-06-28T19:53:35.029Z" }, + { url = "https://files.pythonhosted.org/packages/72/db/9051b36294bdbabaa9c7db57db0fbcdfbd17f7a106c539bb423d0323faea/regex-2026.6.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a71b51dd08b9b62f055fafab3dee8af8bd2ec81b373a44caef18d6c5ca28f43a", size = 489481, upload-time = "2026-06-28T19:53:36.684Z" }, + { url = "https://files.pythonhosted.org/packages/35/3f/24097a3c3ff30f9a639888900faaecabcf5f54a5bc9c851c297e11b349ef/regex-2026.6.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c26a47770d30a0f85c01e261d2a3ebc342c4af6fd666dbd8c1fe4cbf3adf726", size = 291292, upload-time = "2026-06-28T19:53:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cc/e0d762a189cfb4e8926d16e691720690d139a977b38fdb80230c259332ab/regex-2026.6.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e5efbc1af38f97e300d43028e5a92e752d924bcfb7f465d8669d5d5a6e78c233", size = 289232, upload-time = "2026-06-28T19:53:40.181Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c8/ca0ac7f09cc88ca61e0c61c53f7db29334f660ffba5d0b52378e7c44723c/regex-2026.6.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1758df6fdd8c800620a5638958720e8a635e1da49a2f09df2dd63e94a24ec4a", size = 792332, upload-time = "2026-06-28T19:53:41.782Z" }, + { url = "https://files.pythonhosted.org/packages/8e/92/04ae94cbe0dd1f478b2aef6c46f995bb6946d3e338d4b28605478b66a2b7/regex-2026.6.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ad73ecf20c1ef5c975639f8bf845a9370fcf7dada7edc1e3b0bca20e2f8202f6", size = 861743, upload-time = "2026-06-28T19:53:43.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ec/024d7638c807679ff8a0e6081d01d66c7762339af1cac71e45911587ff9a/regex-2026.6.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d80c798b0eec6ea3d45f8816a1e8886c5664615d347d89e8c075b576a1b5a5d", size = 906481, upload-time = "2026-06-28T19:53:44.948Z" }, + { url = "https://files.pythonhosted.org/packages/cd/fd/93bfe5af45f0be4fa8983945455c0e6924e1aeb879cde227958869c1e71c/regex-2026.6.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a361feeaf1b6ba1df060f2ff5c5947092edf537a35ce78e76387ac56d3e0f4a4", size = 799867, upload-time = "2026-06-28T19:53:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/ee/fd/e5d965d41f2398c8ce0f37a4652f03bb297fd009bb796d390134225dda12/regex-2026.6.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b92366d9c8bba9642989534073662abdd9b41faf7603a7ae71597833f3b88f0", size = 773632, upload-time = "2026-06-28T19:53:48.892Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/ff39afaec92b9ee2dba0302a4783976005091681069808938c31cf8df3b6/regex-2026.6.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:11251768cc23f097dd61b18f67966e70f74da822784d17e12a444eb6b29d4288", size = 781669, upload-time = "2026-06-28T19:53:50.693Z" }, + { url = "https://files.pythonhosted.org/packages/45/4e/e2fd4bb8228e10c24af2d7ff867182372190e498eab9fd29cbe54c403c95/regex-2026.6.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad5c67786145ec28a71a267d9f9d92bdc8d70d65541eea852c253f520a01f918", size = 854497, upload-time = "2026-06-28T19:53:52.323Z" }, + { url = "https://files.pythonhosted.org/packages/72/7c/f0340384a973082979064156d05f3d2cc1dced7371efcd7a1b45726a1a8a/regex-2026.6.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f1da438e739765c3e85175ede05816cbede3caaacb1e0680568bda6119bfdfca", size = 763335, upload-time = "2026-06-28T19:53:54.024Z" }, + { url = "https://files.pythonhosted.org/packages/e1/32/90ce0d0898e205506cc22b9c81cfb16b722e06ca5f50fad51c053c2a727b/regex-2026.6.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d98b639046e51c5de64d9f77351532105e99ca271cb6f7640e1f903d6ab63032", size = 844615, upload-time = "2026-06-28T19:53:56.216Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/55abb149599dce1ade687170557129524011eeb3d92afe02429cea7754a2/regex-2026.6.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e164ace4dbab5c6ad4a4ac7c41a2638fe226d0c770a86f2eb041f594bac6ee7", size = 789193, upload-time = "2026-06-28T19:53:57.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ea/cf7f6f6f152e52fdad978b913bf24c14df647eca0f81ef31f3aee0be8982/regex-2026.6.28-cp311-cp311-win32.whl", hash = "sha256:3169a3159e4d99d9ae85ff0ed90ef3b8906cc3152653b6078b842ace6c8f72c3", size = 266731, upload-time = "2026-06-28T19:53:59.938Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cf/a48d8e8d406b22481cad146f48fa0dfca3c5f402b91f26d8e5a0fe4f513d/regex-2026.6.28-cp311-cp311-win_amd64.whl", hash = "sha256:5977295b0a74e8241df8a4b3b27b12412a831f6fa32ee8b755039592cd768c3d", size = 277918, upload-time = "2026-06-28T19:54:01.502Z" }, + { url = "https://files.pythonhosted.org/packages/89/b2/a222392207db7ed86281a732a99f7cf7f2bb35d332799e892b8510be000e/regex-2026.6.28-cp311-cp311-win_arm64.whl", hash = "sha256:f5fbaef40c3e9282ccee4b075f5600a0d858aa0c34147732f1baa69c8188a95d", size = 276876, upload-time = "2026-06-28T19:54:03.411Z" }, + { url = "https://files.pythonhosted.org/packages/da/21/44aa415873032056c43eac21c67285deb2cf66cddb2a964c3cdc8f803efc/regex-2026.6.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:81cc5793ad33a10444445e8d29d3c73e752c8fb2e120772d70fcb6d41df40fe1", size = 490480, upload-time = "2026-06-28T19:54:05.392Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5f/30d4116093c2128099f78b6990dfc1698fdbf3ee528f1e1c647378034c79/regex-2026.6.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e18225243250a1f7d7e5e5d883f3b96465cd79031acf5c6db902b7025f2125d9", size = 292137, upload-time = "2026-06-28T19:54:07.088Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/ca20a0e0de49837e6337603a91ab77556aa27033ac5b975615d98698cfb3/regex-2026.6.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd1638b1c2db1f2d01c182a4b0d3e2e88b0e99910320a745c1727ee3638ddab", size = 289623, upload-time = "2026-06-28T19:54:08.762Z" }, + { url = "https://files.pythonhosted.org/packages/50/11/c013422a7e2c59946df8ac93e792a4922c98287f2a2181341603c78a5d98/regex-2026.6.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4303ebe16b74eeb3fe2715745023266fea92fd44a23f3e7bb2fb48c7a7bbc195", size = 796756, upload-time = "2026-06-28T19:54:10.616Z" }, + { url = "https://files.pythonhosted.org/packages/b0/95/1309645a0e1ee6fb91d954501da57a0b33d50ad2a9acb313702851a7054e/regex-2026.6.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56b856b70b96c381d837f609eee442a1bd320cd2159f5c294b679552fb1a7eaf", size = 865465, upload-time = "2026-06-28T19:54:12.742Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/491802db47c6f5e2904ffa2518ad3ac27fe6bbf5a66d73210a95cc080d47/regex-2026.6.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f74675ab76ab1d005ffba4dee308e53e89efc22be6e9f9fae5b539a3f81bdff2", size = 912350, upload-time = "2026-06-28T19:54:14.508Z" }, + { url = "https://files.pythonhosted.org/packages/5e/60/3ba57840bcc7e2367090360de0c15a5ba6ad22be89314251105f2e943f43/regex-2026.6.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90581684565a93f7258af1e5d3f41ef20d7d7c61f2a428183a342bcb65485e38", size = 801261, upload-time = "2026-06-28T19:54:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/eb/27/af1eb74e9a78c782b3e450b611a595e44906da8a5107e1227f4a7fd0480b/regex-2026.6.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:28f9e6c28f9b90f6f784595a33240a57e181e61b6ee3dc259b25c61e356d1aa3", size = 777072, upload-time = "2026-06-28T19:54:18.128Z" }, + { url = "https://files.pythonhosted.org/packages/20/18/fdd4c883a39e3ed00d669062af1135809bfd3281bf528150849fbd68825b/regex-2026.6.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:378a71d861fc7c8806b04ac5b133d53c0e774f92f5d9663a539872d3fa2b0417", size = 785119, upload-time = "2026-06-28T19:54:20.314Z" }, + { url = "https://files.pythonhosted.org/packages/1c/79/0aabe34b8482dcadf64355f70f96e22eba5ec6c1efb33563f89654f4061c/regex-2026.6.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc199874ecd6267a49b111052250825bfe19b5101b23b2ba80f54efa3e0994e", size = 860118, upload-time = "2026-06-28T19:54:22.368Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2c/c973323306a27c9db7d160e9584eb7e0ece2a96224ccb0d39060558b31f9/regex-2026.6.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b916a10431494ef4b4d62c6c89cab6426af7873125b8cd6c15811bf5fc58eec8", size = 765786, upload-time = "2026-06-28T19:54:24.265Z" }, + { url = "https://files.pythonhosted.org/packages/e3/df/9ca3e378e352242a4cb45573a5e9162c3ee791507702a23966fa559e36b5/regex-2026.6.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e27727fba075f1e4409416d2f537d4c30fc11f012ea507f7bd74d3e19ecb57a", size = 852120, upload-time = "2026-06-28T19:54:25.972Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3e/3e31e255c4971f53cbce6306b5e3c76cbd3735a54f419bb3b2f194e9f68c/regex-2026.6.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:700fc6a7844bb2c4149292ac79d1df8841a00acd4d45cd32c1ebc7bcc1fd0da8", size = 789503, upload-time = "2026-06-28T19:54:27.678Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/d36561c21c3033d7eeb31d51b491916817de7861acefccc5fc9db8a5037c/regex-2026.6.28-cp312-cp312-win32.whl", hash = "sha256:03376d60b6a11aecb88a79fa2be06b40faa01c6693bc31ef69435cd4818b9463", size = 267109, upload-time = "2026-06-28T19:54:29.316Z" }, + { url = "https://files.pythonhosted.org/packages/a0/59/bbbb0591f38b18c65977cd65ce64749eba1c1996c99ac04e900fc30c0dcb/regex-2026.6.28-cp312-cp312-win_amd64.whl", hash = "sha256:fbd2ded482bf99e6651992bbfcde460272724d4bbc49ef3d6b46d9312867ec84", size = 277711, upload-time = "2026-06-28T19:54:31.143Z" }, + { url = "https://files.pythonhosted.org/packages/86/06/be4f6b337d773ae5739a1bc238f97c16926e72017243735853c030f4c628/regex-2026.6.28-cp312-cp312-win_arm64.whl", hash = "sha256:37294d3d7ddb64c7e89184b2894e0f8f0a19c514bc59513d71fe692c3a8d5fc6", size = 277022, upload-time = "2026-06-28T19:54:32.97Z" }, + { url = "https://files.pythonhosted.org/packages/b6/53/d5c1b3cc0b5a0c985563ad6fac93d73ff2b300cb84342d89f044625d6bc7/regex-2026.6.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b295a83426e0e44e9e60fde99789e181bd26788a1890ae7fe2a24c69bb6246ca", size = 490329, upload-time = "2026-06-28T19:54:35.775Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9f/0c3503e819e91ca0e7a901a8e989ebf840ac7c7aea20b1fc7f31b6759f77/regex-2026.6.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c31665c0deb5c111557a1cac8c27bd5629e2f9e7fd5058900a03576c33b601c", size = 292039, upload-time = "2026-06-28T19:54:37.977Z" }, + { url = "https://files.pythonhosted.org/packages/bb/7f/cd004e13fcad23b3794a82307dfd222e6365eb7f598bd3caab148a830bff/regex-2026.6.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6bf295f2c59de77d1ea7de053607ae4dc9ceb3d57bbb6c7ec51ef4acc4ccff94", size = 289488, upload-time = "2026-06-28T19:54:39.545Z" }, + { url = "https://files.pythonhosted.org/packages/73/4c/293fb34586fbcdc47eac436069e9c11f71fae5dadfd4889b475d7d2e5f7a/regex-2026.6.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17c077586770f67e05bbffeba07fbee6b2b22244f4d4caf8d94e59d574befe04", size = 796772, upload-time = "2026-06-28T19:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/c0cd1a90b7d12d9dc155cfc8bdea8df9720988ea5b07e8fa1eccbd0ab2dd/regex-2026.6.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e6cb5a61486f9062397d2e189573b39d38ecfaed698fd9fb6e2756a8ebb8762", size = 865467, upload-time = "2026-06-28T19:54:43.485Z" }, + { url = "https://files.pythonhosted.org/packages/4e/db/0b479973046d005a1eaea299d5d536aeecb9488a16d9cbb8286338102e2d/regex-2026.6.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e86e91a2664f44c3a4e363a7d78fb17c27d5046882e30ea5a877f5e89b28d2ba", size = 912345, upload-time = "2026-06-28T19:54:46.091Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5b/d65adfbd02f32212431bca1f06d1e2eb763a20b12978b454bafaf23dacb7/regex-2026.6.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dfd1331c49233998d84fc5f1f4436cf7a435a7655f6cf0f490229bb5c7254e5", size = 801291, upload-time = "2026-06-28T19:54:48.3Z" }, + { url = "https://files.pythonhosted.org/packages/fc/09/2103686defaf9a0a31c1663782359d5b45f42524c64cca681f5481e44a5e/regex-2026.6.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cadea12805a1bce0b091c302b814207be26fb60a9c0e7f9ad2f9e21790a429fe", size = 777106, upload-time = "2026-06-28T19:54:50.326Z" }, + { url = "https://files.pythonhosted.org/packages/85/5a/b57593c0aa23ed269ec332fbcf07852abcb6b746e811d9464e0d09b4e25f/regex-2026.6.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f2c1682b67ad5d2376498f2a5a2a8f782fa2e4a06d0465b5e357799806e8a20", size = 785175, upload-time = "2026-06-28T19:54:52.172Z" }, + { url = "https://files.pythonhosted.org/packages/79/59/c36e756ad29bf14d7b6c6d7138952476b21f6160286cedb98ac13481c993/regex-2026.6.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:64e142eb55e84868087da1375d7c36ff97d55010951849f515322a91d5fef1b4", size = 860186, upload-time = "2026-06-28T19:54:54.11Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/49808aea0da9649c300139360708fb91b7144be1f962fcebf96755fde948/regex-2026.6.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:abb4daabe7be63273787a62dfd6164dadf8f7a63fbec3d2730e5e5e7126d858c", size = 765754, upload-time = "2026-06-28T19:54:56.04Z" }, + { url = "https://files.pythonhosted.org/packages/be/c5/52bbd436cf2200decdf48825fa38363eaaeebb77011ea9928a1ef9e0b9f2/regex-2026.6.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec2b2ad00ab8c16a2798cc8db80c53c4d5b8b3a2441f6cbaef06625f5ca25854", size = 852085, upload-time = "2026-06-28T19:54:57.988Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c3/0390b66e3019497143fe768b3ba567b64d8b24f3812d09506deb86f4a0f0/regex-2026.6.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bfc9677982c914d9085b8e1c3b3ae6e88f139fb56531c2416d6c8f338093c22b", size = 789600, upload-time = "2026-06-28T19:54:59.977Z" }, + { url = "https://files.pythonhosted.org/packages/88/fd/ab5b03653a244975069fed93d73f4f5f7484c03a84cedb238292510d7182/regex-2026.6.28-cp313-cp313-win32.whl", hash = "sha256:bf54bc693fc4e0530e666ba5ec4bcba14dbe8f66b7cfc15c27317d1a6e40b9a5", size = 267088, upload-time = "2026-06-28T19:55:02.159Z" }, + { url = "https://files.pythonhosted.org/packages/68/55/21022f7d3143210ae8d4ff905c45306237b657375cc0b97883f49db3d423/regex-2026.6.28-cp313-cp313-win_amd64.whl", hash = "sha256:e128feaf65bf3d9eb91bec92322a8f7e4835e9c798f3e9ea4b69f4def85620e3", size = 277680, upload-time = "2026-06-28T19:55:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/7f664804f1aef924542b0b233996b78b3e4d0a52d9951358aac99f129f51/regex-2026.6.28-cp313-cp313-win_arm64.whl", hash = "sha256:695873e0ea8d3815ea9e92e2c68faf039cc450e2c0a62a31afe2049eb11be767", size = 277017, upload-time = "2026-06-28T19:55:06.29Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e1/9eb83518e159d719fd681c4932dc2aaff855ce72451e1d05d69466f25a96/regex-2026.6.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:189dbf9fc4252d9f1352bf4bd1bef885edb6cc4b7341df202a65f821aaa3891c", size = 494195, upload-time = "2026-06-28T19:55:08.292Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e2/e259c5f2f7be269d0e2fb54275c1fa6a13fb47019f389c3f3ae457447825/regex-2026.6.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9277a4c6503390aa39cb4483b87ec0384faee0850a23b5cea33d008b5d8d83f1", size = 293976, upload-time = "2026-06-28T19:55:10.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/9bdf444014d22b045d0c82ca114fac7e07a597b5b5331b7c4ce6328426e2/regex-2026.6.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17eddca4e8ea9af0b5739314776cdf0172a49731ab61f2e1ea66e066ddd46c97", size = 292340, upload-time = "2026-06-28T19:55:11.88Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3a/f49b11e59cbfe187ace0053a460bd72a0169b8cd52e7db9421a074ce7a43/regex-2026.6.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4466b8641e00c697aab5a73150150d2b2ea96b131c595691f42031abafd9f4d", size = 811704, upload-time = "2026-06-28T19:55:13.612Z" }, + { url = "https://files.pythonhosted.org/packages/2f/fb/ad04c39e149bf8b6cf357df5fff78341733ec366780a00c803a36735818c/regex-2026.6.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cfcd4b0bdcf768c498415c170d1ed2a25a99bf0b65fa253bbd02f68ceba6475", size = 871157, upload-time = "2026-06-28T19:55:15.797Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/0e5ba31c11eb8ef7aac19a690c1211fc9aa9990caf09565785ebb0081b9a/regex-2026.6.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80c7adf1ef647f6b1e8aa2ca280e517174cd08bdf7a2e412cdfb68bd6a0917cb", size = 917287, upload-time = "2026-06-28T19:55:18.692Z" }, + { url = "https://files.pythonhosted.org/packages/11/75/6b78df2b858c2fcbbc4858fdc3f2975cf2703be374b2842db7d2c32591a7/regex-2026.6.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a043f5770e82283a22aed4cefef1a4e0f9dd8fd7184cb6ce0ad2e579e2134a9e", size = 816333, upload-time = "2026-06-28T19:55:20.973Z" }, + { url = "https://files.pythonhosted.org/packages/b4/01/ecfe665a3694d5eda9f3ec686c856438ada0943947b6005e90556a1e2cdf/regex-2026.6.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3bd630a8dba06b55254ea5ee862194edab52ec783100d2ef1cd15a9c512fee27", size = 785518, upload-time = "2026-06-28T19:55:23.003Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0a/88f9cd88ff1e82881605c4ffd62d77ee67d051232cfe6f8e9a64b86cf0e8/regex-2026.6.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b77207e3cee13086f1906a6a2a12b41244c577e8ad9370d4b35ae1d548d354f3", size = 801371, upload-time = "2026-06-28T19:55:24.888Z" }, + { url = "https://files.pythonhosted.org/packages/a8/97/601483732f93275482ceb9fed57813dfed7c47d3a019db6ec4a3bb6e23e0/regex-2026.6.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:6de82c268e5d101ee9e3ffd869924aa9a371e3a21e752cf4fa17b6ce50d219f7", size = 866517, upload-time = "2026-06-28T19:55:27.232Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/385c2a0351b994a693453c1d1a6e9af9eb35db3c9460d76b5078acd70c62/regex-2026.6.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b15859e3908544fb99cf47341dcf0bfd089147d258c4c4d8a29e5b087f8085cb", size = 772834, upload-time = "2026-06-28T19:55:29.154Z" }, + { url = "https://files.pythonhosted.org/packages/06/bc/bbf4a5b3b29770d7f307d3c28b5b1bca0105b0cb424be0a4eb1339bc92cf/regex-2026.6.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c91487a917edd48a1ea646fdf60d7936d304f0e686fa7ea8326e47efca51d816", size = 856606, upload-time = "2026-06-28T19:55:32.186Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/51d74fff82f682819979249f8d700267108ba5dc4eb284b0e11b9c85e4b3/regex-2026.6.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4ac65f3e3a99fd8f3a4a74e7a6610acd1ce9dfe9b8a03d346a4922380d68aeb", size = 803475, upload-time = "2026-06-28T19:55:34.328Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3e/6be10cefdc813533fe604dbf5d3c77d2638e7ee658b2749ebadc113b6b2e/regex-2026.6.28-cp313-cp313t-win32.whl", hash = "sha256:3f6316f258bc7e6c9c2acbe9954947bbd397a81be3742a637a555f1855d6618d", size = 269126, upload-time = "2026-06-28T19:55:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3c/32cda905ea1a6eeeb798291c294d8ec66ee0efe0cdba28b061e248b1d396/regex-2026.6.28-cp313-cp313t-win_amd64.whl", hash = "sha256:1484bdd6fba28422df9b5ebb04055b2e1b680e8e4f08490bb21ff0f3cc50d0ab", size = 279961, upload-time = "2026-06-28T19:55:38.456Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b9/69f4e5cd6fbe0bb420cb2dbae441ca118f2495bdda522a74da75aa9829e7/regex-2026.6.28-cp313-cp313t-win_arm64.whl", hash = "sha256:3f15020f0b69cafe57baa067ff65b29acef68ff6b1670a53bef1ca11d708e02d", size = 279266, upload-time = "2026-06-28T19:55:40.62Z" }, + { url = "https://files.pythonhosted.org/packages/3b/fb/fad3b810a5bb1e09b9e5d6913fc6ba88cab738fdf283196827a3c59a4c10/regex-2026.6.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:f7c032b0c8a73739ff8ff1aaf30c281fa19c17bf7f1543256c8507390db7807c", size = 490407, upload-time = "2026-06-28T19:55:42.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/52/b8c79d12276d93e90e707e939b396034c04980caf1235312ef790f8e11fc/regex-2026.6.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f6710f512c57b84f127a23d0f59560a03b64136eff419ae1be5ab557577fe5e3", size = 291988, upload-time = "2026-06-28T19:55:44.549Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/6a911f18279daa8d7bb8b20d771ddb6ef31fabd35f5921f9d3ba21640e80/regex-2026.6.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0013958f427bd82509a186b9ff206d66cb8d60a81fc797a4c717afd18c5b0ba", size = 289704, upload-time = "2026-06-28T19:55:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/fd/22/ad1955c47c669291a05804d53d7071cc0732dfdf166857be38003cedc2d1/regex-2026.6.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f06cdcd6421f8e194ad312ea608020381250df9b8a57661c1b57e9e5273878", size = 797017, upload-time = "2026-06-28T19:55:48.166Z" }, + { url = "https://files.pythonhosted.org/packages/e5/67/a83159ff8703ab4d0c2cf99e76ebf289b7b4a501623241d09f88f3614f80/regex-2026.6.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec9689392f7494ff4e3f8e7e8522f9158f11023f337eaaf04a64542fc45bbf26", size = 866112, upload-time = "2026-06-28T19:55:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/7bff2d6dbbd77421b3274aa51db1c887381cbc5b6eda93598c3e882ea345/regex-2026.6.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aa084684e6d2078bf6139e374d1fc2af5ddc1ac7122759a2db716d68169f6fd0", size = 911554, upload-time = "2026-06-28T19:55:53.707Z" }, + { url = "https://files.pythonhosted.org/packages/29/44/ae59c3826e7ba492e56795cdf74ea2a7b5b7c5ea116afb79ee4956a5dff1/regex-2026.6.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40455e6840dc4e96a6fe50f4cedc957de2752c954d91e789812be55d49be199a", size = 800665, upload-time = "2026-06-28T19:55:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/d6/19/6fd033d2ab00f35d445aaeaf3307c1e721424dcbfd48f6f65c857cb939cf/regex-2026.6.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:530b5c223b9ca5dd8370ac502e080aee0e4ded32be987c6564b425fb5523d581", size = 777243, upload-time = "2026-06-28T19:55:57.909Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9d/99730f26df4938049ab1e652ca75e967b4c6739444e18d9707bfdb8af20c/regex-2026.6.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ed273ecd1a89be84466c1749bfe58609cc2a32b5d5e05006c4625ba96411b", size = 785784, upload-time = "2026-06-28T19:56:00.072Z" }, + { url = "https://files.pythonhosted.org/packages/48/49/105cd57162f5fc5c04cc917a1388a060cf8427e5c14353cd9044660fbf4d/regex-2026.6.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ab0d5344311fc8e8667078942056c3b9c9b4a4b1cc99f2eb8a5af54554f4acc", size = 860914, upload-time = "2026-06-28T19:56:02.017Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/788245a95b69018f58bff2f4fd27d007cacaea088cdb390979743f1b2571/regex-2026.6.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:eacb79625323d9f7e7925366b917f492b8356fad58f5dc4fa12ff8c21d8f4ca9", size = 765915, upload-time = "2026-06-28T19:56:05.021Z" }, + { url = "https://files.pythonhosted.org/packages/ca/01/292065a39a004b05e67a337b18213670a7cb919d6856ac2d7df7f1a10dbb/regex-2026.6.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20f4d87702702aa1d572721e146f301660c50eef6fd6cb596e48a22b0ace17db", size = 851404, upload-time = "2026-06-28T19:56:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/98/9e/a93d865db0e13483ae1a01d81e2ce16d4a7fe2f9b9fe4aac4cc08590b136/regex-2026.6.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e693940a3b9e6d6e4dc2a54ecaa74b74934f77af1ef95f518a74261ef7cc1bc", size = 789373, upload-time = "2026-06-28T19:56:09.894Z" }, + { url = "https://files.pythonhosted.org/packages/82/0c/38b1685ad4017d78efbc8fa7dbbf96d8113b53750c8aa2d3609defd46605/regex-2026.6.28-cp314-cp314-win32.whl", hash = "sha256:234a51e20ebc18ab83b2c0600cf28f2e884560a0e00f743878f0b7d8e7c4cf03", size = 272496, upload-time = "2026-06-28T19:56:11.83Z" }, + { url = "https://files.pythonhosted.org/packages/55/50/e19f261ff9ba9b50722a529e09b1743ecf65eb348be99d0fd2cd7fcede1c/regex-2026.6.28-cp314-cp314-win_amd64.whl", hash = "sha256:7b15c437bc4604f03ceb3f8d37eae2f8930e320e1bc556b259848c639d9eec1a", size = 280754, upload-time = "2026-06-28T19:56:13.758Z" }, + { url = "https://files.pythonhosted.org/packages/36/b8/c9e68f3a9e33be73f20990b2c065b144ff2d0aa242608a950d8c4f3b56e8/regex-2026.6.28-cp314-cp314-win_arm64.whl", hash = "sha256:c6e6f790d01380a74ad564f216c533b86504afb61bf66f2b2e11e7f1a3e287a7", size = 280979, upload-time = "2026-06-28T19:56:15.928Z" }, + { url = "https://files.pythonhosted.org/packages/03/e6/21c425a37880c650d007c4171c6a80325446d830d85f5fbf335e7205b1e7/regex-2026.6.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3527a72adcbe9e3600f1553b497d397c1a371d227580d41d96c3c5964109b65c", size = 494282, upload-time = "2026-06-28T19:56:18.049Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/6647a7ccf5ffff995ba955a0b7d766440f4e58ce1666549c8ee998f2b972/regex-2026.6.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a644f6408692812f5ead82519eed680e08d5d546fddbd9f7d9514e3c73899aa5", size = 293977, upload-time = "2026-06-28T19:56:20.145Z" }, + { url = "https://files.pythonhosted.org/packages/8c/dc/a3e141a4eaf125e50f63105570c01fa477c06ac5259dcfa95e9b90760e84/regex-2026.6.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8e2fae6bb883648346f84db270dc9aafc29d8e895f62b88a75ccc83b09519820", size = 292432, upload-time = "2026-06-28T19:56:22.345Z" }, + { url = "https://files.pythonhosted.org/packages/35/ee/2ac1a6b9f167f8ff69f5a789938cc103b60cff41b24a6990daced8b88e34/regex-2026.6.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debe623e09cee97ef9404575e936c610aac9bb08358c5099aaef14644a6871f2", size = 811877, upload-time = "2026-06-28T19:56:25.056Z" }, + { url = "https://files.pythonhosted.org/packages/df/7b/9a5505ee92180bcae300b1018b9ff3d3c19962436e66f2505f255e9fde35/regex-2026.6.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc579c91fb4605773483a8d940b136bcc5b854fff44fa14a1572a038f46563f1", size = 871212, upload-time = "2026-06-28T19:56:27.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/4d/d61a702a9f9d1bd29b22cbef1aed6d477baa961232a7eb4d91b7775b0b3e/regex-2026.6.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7c42be203d84ecf7d487ff23f8a61ef0eb0534fa0fc317a2fce8c065d20618f", size = 917507, upload-time = "2026-06-28T19:56:29.762Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/1308066f5966b65fbb6905b99ba37e9f1cd753dd0ac08485f8257334ee92/regex-2026.6.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8184b4e2fdaf9cdfe77e38f15a4d9dc149168c9c29eb0ea17c5481d3bb80546", size = 816389, upload-time = "2026-06-28T19:56:32.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/57ce2cb8d714ee0b7f11c7ee4cfe2af66df2b90f147feadcb538609a3a02/regex-2026.6.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:697f103104f5872d64078d8eeac59979960be8ee76115a2d3f31096312e2a400", size = 785890, upload-time = "2026-06-28T19:56:34.492Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fd/1d5350d3a8a327bff0fccacb911732baf7b5b6f5529c0e3fa602a23e7dad/regex-2026.6.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:714d2b1aa29beef0ddfcdc72ad0771c05326551a8bb0680b0ddf74bfaad87387", size = 801451, upload-time = "2026-06-28T19:56:36.749Z" }, + { url = "https://files.pythonhosted.org/packages/f3/79/3c9e4f8a0306e030ad5a43bbbc01625fb28d58a813bc52d42fd1cc63fb2e/regex-2026.6.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0f09f62e450cc2f113018cc8412aeea3a120a04e1ca7e801a0d441583f9a3b06", size = 866504, upload-time = "2026-06-28T19:56:38.994Z" }, + { url = "https://files.pythonhosted.org/packages/65/12/f747de475b54f4709efb24dd0fbc8467c64cec91f5db0d047b079646ee78/regex-2026.6.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:731ea12d5aeb2577eaef2393d6428b995f76eb35f68a89e03e15a97719d1de19", size = 773047, upload-time = "2026-06-28T19:56:41.061Z" }, + { url = "https://files.pythonhosted.org/packages/58/3c/f02f860e0500c1b2d61a79dec7e214b37fb9656281dcddc92397edf96678/regex-2026.6.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:51e952c8783eabd4706d0f63922f219bcfc1bef9b8cb35941c0d1a0396578858", size = 856665, upload-time = "2026-06-28T19:56:43.466Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6c/28b3fa222513484be9dee26b7222bda109056c43ea28aa2314262ca48816/regex-2026.6.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43248fe4c0ab8fbb223588a0795b11268940072c97bba30ea8f9b49d8cdfde34", size = 803573, upload-time = "2026-06-28T19:56:45.791Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/8f86cf1a1fd85c5ab0c503c9fe4607ad4ad48978b2d8b435d94465e134c7/regex-2026.6.28-cp314-cp314t-win32.whl", hash = "sha256:fc1eddc25ad23c0f1344ab280d961ac595ead48292d7c779497975942373f493", size = 274515, upload-time = "2026-06-28T19:56:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/0f/de/f8613c03b36786ddef2c930d28f9bcae861fcd541cc9203a870956cf1e83/regex-2026.6.28-cp314-cp314t-win_amd64.whl", hash = "sha256:ede8d8e53b6dde0a50f7eca902f0af76d87ab02a55aba7542da68ae3e5dfe83d", size = 283650, upload-time = "2026-06-28T19:56:50.614Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f3/f5ec86839bbabe33b6dee649b62ff9a445d43de6b0ad780cf6b83c56f61e/regex-2026.6.28-cp314-cp314t-win_arm64.whl", hash = "sha256:4da6f6a72f8700b97a1a765e837fb7d5750bfd9f13acea7bae498f573e3a70a8", size = 283338, upload-time = "2026-06-28T19:56:52.879Z" }, ] [[package]] @@ -4415,7 +4474,7 @@ wheels = [ [[package]] name = "rpds-py" -version = "2026.5.1" +version = "2026.6.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -4424,137 +4483,123 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version == '3.11.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/a0/acf8b6fc20bfdcd3a45bd3f57680fb198e157b7e997b9123b10763798bd2/rpds_py-2026.5.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036", size = 355609, upload-time = "2026-05-28T11:58:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/b6/95/f8203fd997484b1690a6869cd0e503b6c3c6be55b0ecc36d1a491fe742f0/rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc", size = 348460, upload-time = "2026-05-28T11:58:52.374Z" }, - { url = "https://files.pythonhosted.org/packages/33/8c/b47326ad2f0be545a5e5c1a55937a12afaea7d392ba2837bb9680f57e6c9/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164", size = 381031, upload-time = "2026-05-28T11:58:53.775Z" }, - { url = "https://files.pythonhosted.org/packages/22/0b/e83bbd97ffac6f6389b605cd4e1c8ac5761dc7e977769c9255d8c5adb7bd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead", size = 387121, upload-time = "2026-05-28T11:58:55.243Z" }, - { url = "https://files.pythonhosted.org/packages/fd/0e/d285d1bc8864245919c61e1ca82263e4a66d337759c3a4cef72766ff9afc/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece", size = 501026, upload-time = "2026-05-28T11:58:56.788Z" }, - { url = "https://files.pythonhosted.org/packages/86/06/ccb2109a1e543437b5e43816f2b43b9554cc6783145528a4e3711e05c011/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb", size = 391865, upload-time = "2026-05-28T11:58:58.298Z" }, - { url = "https://files.pythonhosted.org/packages/3d/33/237173db1cfef10105b3839a24de00eb8d2a523711add4632447cdf0aedd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda", size = 378012, upload-time = "2026-05-28T11:58:59.589Z" }, - { url = "https://files.pythonhosted.org/packages/97/64/1eae54e34d5161f9969295e80bd6b62a55f2b6ac5f2a5b60d02c2140e758/rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a", size = 391111, upload-time = "2026-05-28T11:59:01.104Z" }, - { url = "https://files.pythonhosted.org/packages/d8/34/5bb334a5a0f65d77869217c4654f34c78a7d11b93938a3c076a2edeafc52/rpds_py-2026.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0", size = 409225, upload-time = "2026-05-28T11:59:02.433Z" }, - { url = "https://files.pythonhosted.org/packages/16/0f/007ec21283b5b040b4ec3bd95e0402591e22bfa7d5c93dfe01c465c2d2d7/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a", size = 556487, upload-time = "2026-05-28T11:59:04.012Z" }, - { url = "https://files.pythonhosted.org/packages/ff/10/5437c94508169b6b22d8418fef7a66e9ffb5f3b9e9c94460f2eedafe06ff/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2", size = 620798, upload-time = "2026-05-28T11:59:05.485Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d5/9937dce4d6bda74157b954e7d1460db05a22f5929dccfeeba1ed27a93df0/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2", size = 584053, upload-time = "2026-05-28T11:59:06.837Z" }, - { url = "https://files.pythonhosted.org/packages/6c/31/750617dd0ae1752471bf43f9e41d263398fae7cde7849d23b8574a70e617/rpds_py-2026.5.1-cp311-cp311-win32.whl", hash = "sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f", size = 214390, upload-time = "2026-05-28T11:59:08.402Z" }, - { url = "https://files.pythonhosted.org/packages/3c/bb/3dcab0e1d9516303f2eb672a5d6f62eca5a69e2886301e9c8c54b520c39b/rpds_py-2026.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a", size = 231097, upload-time = "2026-05-28T11:59:09.786Z" }, - { url = "https://files.pythonhosted.org/packages/49/d6/c6bbf5cb1cf12b9732df8074b57f6ef8341ba884c95d40632ae8bddb44e4/rpds_py-2026.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b", size = 226361, upload-time = "2026-05-28T11:59:11.079Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" }, - { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" }, - { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" }, - { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" }, - { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" }, - { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" }, - { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" }, - { url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" }, - { url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" }, - { url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" }, - { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, - { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, - { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, - { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, - { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, - { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, - { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, - { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, - { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, - { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, - { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, - { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, - { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, - { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, - { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, - { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, - { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, - { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, - { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, - { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, - { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, - { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, - { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, - { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, - { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, - { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, - { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, - { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, - { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, - { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, - { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, - { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, - { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, - { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, - { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, - { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, - { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, - { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, - { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, - { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, - { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, - { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, - { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, - { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, - { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, - { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, - { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, - { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, - { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, - { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, - { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, - { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, - { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, - { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, - { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, - { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, - { url = "https://files.pythonhosted.org/packages/42/56/3fe0fb34820ff667be791b3a3c22b85e8bcba54e9c832f47438c191fa7be/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea", size = 357151, upload-time = "2026-05-28T12:01:53.43Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/3eb9ccdb9f143b8c9b003978898cb497f942a324c077401e6b8834238e63/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb", size = 350195, upload-time = "2026-05-28T12:01:54.901Z" }, - { url = "https://files.pythonhosted.org/packages/a7/24/dbda232bc4f3ed732120692ab0d2c8402cb020516556d8bee622dcef2413/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df", size = 381850, upload-time = "2026-05-28T12:01:56.601Z" }, - { url = "https://files.pythonhosted.org/packages/40/30/32e769839a358f78810c234f160f2cc21d1e4e47e1c0e0e0d535be5a0219/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7", size = 387899, upload-time = "2026-05-28T12:01:58.212Z" }, - { url = "https://files.pythonhosted.org/packages/ab/86/ec84d243aadb3b34b71dd26a010d0930b2d284ff5fc9a69fec53810ee6fd/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc", size = 501618, upload-time = "2026-05-28T12:01:59.888Z" }, - { url = "https://files.pythonhosted.org/packages/74/25/b60e52686bbff777a64f9e4f4d3dd57980dc846913777177a2c92e4937aa/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162", size = 394003, upload-time = "2026-05-28T12:02:01.482Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c7/b3a6a588cc2219510ef3f42e207483a93950bedd1e3a0fd4015c95cff9e5/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251", size = 379778, upload-time = "2026-05-28T12:02:03.197Z" }, - { url = "https://files.pythonhosted.org/packages/31/00/c7dba3fc8a3da8cb3f6db1eb3386be4d79c2e97c6890d20eb9ac66ae8c43/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a", size = 392359, upload-time = "2026-05-28T12:02:04.817Z" }, - { url = "https://files.pythonhosted.org/packages/93/dd/472ba494c70753f93745992c99855bee0636daf74e6984e5e003f150316f/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b", size = 412820, upload-time = "2026-05-28T12:02:06.401Z" }, - { url = "https://files.pythonhosted.org/packages/1d/6f/93831a3bfe789542ed0c1d0d74b78b440f055d6dc3ea4640eba2d95e6e23/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34", size = 557243, upload-time = "2026-05-28T12:02:08.013Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ff/0b3d604614ffc77522c6b288fdbce68957eb583da1002aa65ba38ac0ee40/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c", size = 623541, upload-time = "2026-05-28T12:02:09.661Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ea/e7b0251441da9adfeaebcf29601d10f2a1455fcf0772fae9e7e19032bd96/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049", size = 586326, upload-time = "2026-05-28T12:02:11.47Z" }, + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, ] [[package]] @@ -4571,65 +4616,63 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.15" +version = "0.15.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, - { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, - { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, - { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, - { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, - { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, - { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, - { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, - { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, - { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, ] [[package]] name = "s3transfer" -version = "0.18.0" +version = "0.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/1f/12417f7f493fc45e1f9fd5d4a9b6c125cf8d2cf3f8ddbdfab3e76406e9d6/s3transfer-0.18.0.tar.gz", hash = "sha256:3760b8b7ec1315da54048b2d626276732bee4300d054d492d4e1d43e20d4ecbd", size = 160560, upload-time = "2026-05-28T19:39:09.124Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/94/dcdaeb1713cab9c84def276cfac7388b17c7d9855bbcfe88d77e4dbafd44/s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685", size = 165171, upload-time = "2026-06-16T19:44:51.599Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/58/a58fc997655386daa2e25784e30c288aa3e3819e401f77029ee4899fb55a/s3transfer-0.18.0-py3-none-any.whl", hash = "sha256:239c13b09e65ad0346e1be7348b8a202dcad44ac7ea7c6eb858fc881dce739b6", size = 88572, upload-time = "2026-05-28T19:39:07.999Z" }, + { url = "https://files.pythonhosted.org/packages/46/5f/4c174edad94f82de888ac00a5ddd8d07b35609b6c94f0bdf4d74af57703e/s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262", size = 90101, upload-time = "2026-06-16T19:44:50.439Z" }, ] [[package]] name = "safetensors" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, - { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, - { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, - { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, - { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, - { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, - { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, - { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, - { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, ] [[package]] @@ -4693,8 +4736,10 @@ resolution-markers = [ dependencies = [ { name = "joblib", marker = "python_full_version >= '3.11'" }, { name = "narwhals", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } @@ -4795,14 +4840,10 @@ name = "scipy" version = "1.17.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -4868,26 +4909,85 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, ] +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14'", +] +dependencies = [ + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +] + [[package]] name = "sentence-transformers" -version = "5.5.1" +version = "5.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "torch" }, { name = "tqdm" }, { name = "transformers" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/d4/7ef93157485e978c016f49da05363c1e4e7237beb5343b64b5631101f0f1/sentence_transformers-5.5.1.tar.gz", hash = "sha256:02b7740dfc60bdbbcb6061625f5d97a5c1a4e2d3baac5f9391b912bb5eae2290", size = 445161, upload-time = "2026-05-20T07:37:44.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/56/d2cb00765a6b15c994a7fccf20f9032f16e8193ca49147cb5155166ad744/sentence_transformers-5.6.0.tar.gz", hash = "sha256:0e7164d051e416c1853ade7c274ff52af3f9da0f4be7f0b83d734c27699e1057", size = 453194, upload-time = "2026-06-16T14:01:56.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/03/ee99a6b030e7a2e056547729f8a4709dd93e13d9c6f07590f74c395c4017/sentence_transformers-5.5.1-py3-none-any.whl", hash = "sha256:4fe11d433badc5282d32f7fc08bc714216b7a5aca426f9df77a45a554756deb7", size = 588887, upload-time = "2026-05-20T07:37:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/76/c1/dc1582b79e9a2eb0cddf9559cd9bcdff084f541d6fe881fdd9d98630dba7/sentence_transformers-5.6.0-py3-none-any.whl", hash = "sha256:d2075b5e687a1611005e20ab04a6846994d51adfcf39610aed066af3c0c0b81f", size = 596411, upload-time = "2026-06-16T14:01:55.103Z" }, ] [[package]] @@ -5070,14 +5170,14 @@ wheels = [ [[package]] name = "tinycss2" -version = "1.4.0" +version = "1.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, ] [[package]] @@ -5222,7 +5322,8 @@ version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pillow" }, { name = "torch" }, ] @@ -5259,31 +5360,31 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.6" +version = "6.5.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/57/6d7303a77ae439d9189108f76c0c4fd89ee5e2cc8387bffb55232565c4ed/tornado-6.5.6.tar.gz", hash = "sha256:9a365179fe8ff6b8766f602c0f67c185d778193e9bdd828b19f0b6ed7764177d", size = 518139, upload-time = "2026-05-27T15:35:54.646Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/0d/b4f481e18c5a51864e6d12b9a05ecf72919696680b747c958c3fc1f4fbae/tornado-6.5.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:65fcfaafb079435c2c19dc9e07c0f1cf0fa9051759ed0a7d0a3ba7ea7f64919c", size = 447737, upload-time = "2026-05-27T15:35:38.122Z" }, - { url = "https://files.pythonhosted.org/packages/9e/9c/5430c39fcab1144d35860f457b15e9c08b4bc7ac86764354204e983d6183/tornado-6.5.6-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:38bc01b4acacded2de63ae78023548e41ebe6fbed3ec05a796d7ae3ad893887e", size = 445899, upload-time = "2026-05-27T15:35:40.519Z" }, - { url = "https://files.pythonhosted.org/packages/8b/79/fa7e14a2f939c807a8d30619b4eb604eab219601b78792516ebe22d40cf9/tornado-6.5.6-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b942e6a137fda31ff54bf8e6e2c8d1c37f1f50583f3ed53fb840b53b9601d104", size = 448964, upload-time = "2026-05-27T15:35:42.106Z" }, - { url = "https://files.pythonhosted.org/packages/a7/71/bd67d5f5199f937dafe03a49a37989f60f600ff6fef34c79412a829d97bd/tornado-6.5.6-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8666946e70171b8c3f1fc9b7876fac492e84822c4c7f3746f4e8f8bc9ac92a79", size = 449935, upload-time = "2026-05-27T15:35:43.906Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a4/c24388c9cf5b3c3a513b56a158af9f23092c9a2810d789e294310797df21/tornado-6.5.6-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c34cfab7ad6d104f052f55de06d39bbafc5885cfeb4da688803308dbcfa90b7", size = 449767, upload-time = "2026-05-27T15:35:45.793Z" }, - { url = "https://files.pythonhosted.org/packages/a5/eb/6a07ad550c3f7b37244bd0becdf293ec3d3e961783d8b720a97df50de1b2/tornado-6.5.6-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:385f35e4e22fb52551dfcda4cdc8c30c61c2c001aef5ddad99cdfe116952efd3", size = 449174, upload-time = "2026-05-27T15:35:47.485Z" }, - { url = "https://files.pythonhosted.org/packages/bb/84/3469e098dccdb6763130e06aacd786bb4363fca7b590a55c101ddf34ed30/tornado-6.5.6-cp39-abi3-win32.whl", hash = "sha256:db475f1b67b2809b10bb16264829087724ca8d24fe4ed47f7b8675cae453ef86", size = 450230, upload-time = "2026-05-27T15:35:49.322Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3c/273a04e0b9dd9016f1685cca0c1c8795a71ac88a34a8c889a0b443483226/tornado-6.5.6-cp39-abi3-win_amd64.whl", hash = "sha256:6739bf1e8eb09230f1280ddbd3236f0309db70f2c551a8dbc40f62babdf82f79", size = 450667, upload-time = "2026-05-27T15:35:51.194Z" }, - { url = "https://files.pythonhosted.org/packages/02/98/0cffe22a224f60c5fb1e3aa0b76f9da2e1ca78b0e9545e3d077c68ce60a7/tornado-6.5.6-cp39-abi3-win_arm64.whl", hash = "sha256:2543597b24a695d72338a9a77818362d72387c03ae173f1f169eadc5c91466ac", size = 449690, upload-time = "2026-05-27T15:35:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, + { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, + { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, ] [[package]] name = "tqdm" -version = "4.67.3" +version = "4.68.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, ] [[package]] @@ -5297,12 +5398,13 @@ wheels = [ [[package]] name = "transformers" -version = "5.10.1" +version = "5.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, @@ -5311,9 +5413,9 @@ dependencies = [ { name = "tqdm" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/d3/5b7c2f1a52ff0e57355efdc21554aab7e4602f6592ab4582a34c988ab956/transformers-5.10.1.tar.gz", hash = "sha256:31112d1dcdfcf9934242acbba891f44e2279ff74b9b8ba4595640e0e04195a3a", size = 8798372, upload-time = "2026-06-03T15:37:03.289Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/e1/720ff7ff666b04279fea5bb7ac3ef8675e98f0ddbc1b8cb8bc9f3889d62e/transformers-5.13.0.tar.gz", hash = "sha256:940c1428e42a4238f9ccf0cd41e63c590701aa63c19fd2ce3d7d602222d68495", size = 9195801, upload-time = "2026-07-03T16:05:39.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/8c/3119596c7fcd9b8b8d924d5b5ba1bfdfafd6d8738bdd81ca09c60b4a38b3/transformers-5.10.1-py3-none-any.whl", hash = "sha256:ccb919ea1b77338b44d0d45d23f7472081906b1bb6ed8e5f5cf4d692d1da03d4", size = 11003770, upload-time = "2026-06-03T15:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3a/d99704c5effe10c6339c98cb236259161103e159bb99a78468b6729572ec/transformers-5.13.0-py3-none-any.whl", hash = "sha256:8adbc1d20bd5463cd6876b2eb7cb31971e1065788e7dc6bc12bab597a7c504b7", size = 11503730, upload-time = "2026-07-03T16:05:35.569Z" }, ] [[package]] @@ -5339,17 +5441,17 @@ wheels = [ [[package]] name = "typer" -version = "0.25.1" +version = "0.26.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, - { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, + { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" }, ] [[package]] @@ -5366,11 +5468,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] @@ -5405,7 +5507,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.4.2" +version = "21.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -5414,9 +5516,9 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0d/4e93c8e6d1001a75763f87d8f5ecda8ebc7f4aa2153dddfaf4ae8892821a/virtualenv-21.4.2.tar.gz", hash = "sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c", size = 7613326, upload-time = "2026-05-31T17:01:22.827Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/65/ec1d92091671e6407d3e7c1f5801413bb7b2b57630a50cae7750456ba0ed/virtualenv-21.6.0.tar.gz", hash = "sha256:e18a4d750f2b64dea73e72ffde3922f3c52365fabdc8157ebd3da20d031c4734", size = 5526111, upload-time = "2026-07-06T22:49:56.972Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/c4/557dc082be035381b85fdb2b74e21d3d21b57750b74f2b47a32f3a639ff9/virtualenv-21.4.2-py3-none-any.whl", hash = "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae", size = 7594079, upload-time = "2026-05-31T17:01:20.735Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e7/2fbd0cc1653c53eed8f10670538bb547de2b3e37aacad283faa82a71094b/virtualenv-21.6.0-py3-none-any.whl", hash = "sha256:bce9d097950fef9d81129b333babfb7767072850c2f1acce0ec536708401bfd1", size = 5506216, upload-time = "2026-07-06T22:49:54.941Z" }, ] [[package]] @@ -5453,11 +5555,11 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.7.0" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] [[package]] From 5a1015ba72ec98170b3c10b737ecaa5b7eb3869b Mon Sep 17 00:00:00 2001 From: Will Jones Date: Thu, 30 Jul 2026 16:50:05 -0700 Subject: [PATCH 21/29] docs(python): fill gaps in the Python API reference (#3746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docs/src/python/python.md` is the whole Python API reference, but it is maintained by hand and had drifted from the public API. Anything not listed there simply doesn't get rendered, so a number of public, documented, tested APIs were invisible to users — most notably branch management, where `diff` and `merge` live. I audited every public symbol reachable from `lancedb` and its subpackages against the `:::` directives on the page. This adds the missing ones: - **Branching** — `Branches`, `AsyncBranches` (`list` / `create` / `checkout` / `delete` / `diff` / `merge`) - **Tables** — `TableStatistics` (returned by `Table.stats()`; the fragment-level stats classes were already listed) - **Full text queries** — `FullTextQuery`, `MatchQuery`, `PhraseQuery`, `BoostQuery`, `MultiMatchQuery`, `BooleanQuery`, `FullTextOperator`, `Occur` - **Querying** — `LanceEmptyQueryBuilder`, `LanceTakeQueryBuilder`, `AsyncTakeQuery` - **Indices** — `Fm` (the FM-index for substring search), `IndexConfig` - **Blobs** — `blob`, `BlobType`, `BlobFile` - **Namespaces** — `connect_namespace`, `connect_namespace_async`, and both namespace connection classes - **Remote config** — `TlsConfig`, `HeaderProvider`, `OAuthConfig`, `OAuthFlowType` - **Rerankers** — the `Reranker` base class plus `JinaReranker`, `RRFReranker`, `MRRReranker`, `AnswerdotaiRerankers`, `VoyageAIReranker`, `WatsonxReranker` (5 of 12 were listed) - **Embeddings** — `get_registry`, `register`, and the 14 embedding functions that were missing (3 of 17 were listed) - **PyTorch** — `StreamingDataset` and the permutation API it is built on - **Misc** — `Session`, `tokenize`, `FtsToken`, `pydantic.Vector`, `pydantic.MultiVector`, `instrument_lancedb_metrics`, and the two exception types It also repairs cross-references in docstrings that no longer resolve: links into guide pages that have since moved to lancedb.com (`querying-an-ann-index`, `experimental-full-text-search`), `lance.dataset` references with no inventory behind them, and the relative targets `[Table](Table)` and `[PyArrow Table](pyarrow.Table)`. Deliberately left out: concrete implementation classes reached through their abstract base (`LanceTable`, `LanceDBConnection`, `RemoteDBConnection`), query base classes already covered by `inherited_members: true`, and internal plumbing such as `FullTextSearchQuery` and `ColumnOrdering`. ## Testing The docs job only runs on pushes to `main`, so I built the site locally and compared against a build of `upstream/main`: every added entry resolves, and no symbol that was rendered before stopped being rendered when the four packages moved to automodule. `mkdocs build --strict` exits 0 on this branch, against 61 warnings on `main`. ## Also in this PR `lancedb.index`, `lancedb.embeddings`, `lancedb.remote` and `lancedb.rerankers` are now rendered by a single mkdocstrings directive each, driven by the module's `__all__`, rather than a hand-maintained list. These four are where most of the drift was, and `__all__` is harder to forget than a docs page. `lancedb.embeddings` had no `__all__`; without one mkdocstrings renders no members at all for a re-export package, so one is added. AGENTS.md gains a section on how the page is wired up and how to build the docs locally. Rendering all that code for the first time surfaced ~100 more build warnings, which would have made #3707 (turning on `mkdocs build --strict`) harder to land, so the warning backlog is cleared here too. 97 of the 158 warnings were one systematic false positive — griffe cannot see the generated `__init__` of a pydantic dataclass, so every documented parameter looks unknown — switched off via `warn_unknown_params`. The remaining 61 came from 15 docstrings with real bugs: prose trailing a `Parameters` section (we were rendering parameters called `The`, `you` and `To`), types dropped because numpydoc needs spaces around the colon, `num_partitions, default sqrt(num_rows)` parsing as a list of names and inventing a `default` parameter, and one parameter indented five spaces. `mkdocs build --strict` now exits 0. --- #3747 (the coverage test that keeps this from happening again) is stacked on this branch, so review it after this one. --------- Co-authored-by: Claude Opus 5 (1M context) --- AGENTS.md | 29 ++++ docs/mkdocs.yml | 5 + docs/src/js/_media/CONTRIBUTING.md | 2 +- docs/src/python/python.md | 163 ++++++++++++------ nodejs/CONTRIBUTING.md | 2 +- python/python/lancedb/db.py | 4 +- python/python/lancedb/embeddings/__init__.py | 29 ++++ python/python/lancedb/embeddings/bedrock.py | 10 +- python/python/lancedb/embeddings/cohere.py | 8 +- python/python/lancedb/embeddings/colpali.py | 4 +- .../python/lancedb/embeddings/gemini_text.py | 6 +- python/python/lancedb/embeddings/gte.py | 8 +- .../python/lancedb/embeddings/instructor.py | 16 +- python/python/lancedb/embeddings/jinaai.py | 4 +- .../embeddings/sentence_transformers.py | 8 +- python/python/lancedb/embeddings/voyageai.py | 4 +- python/python/lancedb/index.py | 49 +++--- python/python/lancedb/permutation.py | 17 +- python/python/lancedb/query.py | 31 ++-- python/python/lancedb/remote/__init__.py | 3 + python/python/lancedb/remote/errors.py | 4 +- python/python/lancedb/remote/table.py | 8 +- python/python/lancedb/rerankers/__init__.py | 3 + python/python/lancedb/table.py | 43 +++-- 24 files changed, 309 insertions(+), 151 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 14ec35441..21631a2cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,6 +92,8 @@ Python bindings changes: * Should use `LOOP.run()` to call the corresponding `AsyncTable` method. 6. Add concrete sync method to `RemoteTable` class in `python/python/lancedb/remote/table.py`. 7. Add unit test in `python/tests/test_table.py`. +8. If you added a new public class or module-level function (not just a method on an + existing class), expose it in the API reference. See "Python API reference" below. TypeScript bindings changes: @@ -103,6 +105,33 @@ TypeScript bindings changes: 5. Add test in `nodejs/__test__/table.test.ts`. 6. Run `npm run docs` to generate TypeScript documentation. +## Python API reference + +`docs/src/python/python.md` is the entire Python API reference. It is maintained by +hand, and anything not listed there is not rendered at all, so new public classes and +module-level functions have to be added explicitly. How depends on the module: + +* `lancedb.index`, `lancedb.embeddings`, `lancedb.remote`, and `lancedb.rerankers` are + rendered by a single directive each, driven by the module's `__all__`. Add the new + name to `__all__` and it appears; forget, and it is silently omitted. +* Everything else (`lancedb`, `lancedb.table`, `lancedb.query`, `lancedb.db`, ...) is + listed symbol by symbol. Add a `::: lancedb..` line to the matching + section, and remember that the page separates synchronous and asynchronous APIs. + +Deliberately undocumented: concrete implementations reached through an abstract base +(`LanceTable`, `LanceDBConnection`, `RemoteDBConnection`), query base classes already +covered by `inherited_members`, and internal helpers. + +Cross-references in docstrings use mkdocstrings syntax, `[text][lancedb.table.Table]`. +Plain relative links such as `[Table](Table)` do not resolve. To check your work: + +```shell +pip install -r docs/requirements.txt +cd docs && PYTHONPATH=. mkdocs build +``` + +The docs site only builds on pushes to `main`, so this is not covered by PR CI. + ## Review Guidelines Please consider the following when reviewing code contributions. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 324b6fda5..ad6fc4b65 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -51,6 +51,11 @@ plugins: paths: [../python/python] options: docstring_style: numpy + docstring_options: + # Attributes documented in a `Parameters` section, and pydantic + # dataclasses whose `__init__` griffe cannot see statically, both + # trip this check. It reports nothing actionable here. + warn_unknown_params: false heading_level: 3 show_signature_annotations: true show_root_heading: true diff --git a/docs/src/js/_media/CONTRIBUTING.md b/docs/src/js/_media/CONTRIBUTING.md index d784b9f75..2faa8cd37 100644 --- a/docs/src/js/_media/CONTRIBUTING.md +++ b/docs/src/js/_media/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing to LanceDB Typescript This document outlines the process for contributing to LanceDB Typescript. -For general contribution guidelines, see [CONTRIBUTING.md](../CONTRIBUTING.md). +For general contribution guidelines, see [CONTRIBUTING.md](https://github.com/lancedb/lancedb/blob/main/CONTRIBUTING.md). ## Project layout diff --git a/docs/src/python/python.md b/docs/src/python/python.md index dd60451cc..36044d35d 100644 --- a/docs/src/python/python.md +++ b/docs/src/python/python.md @@ -26,6 +26,18 @@ is also an [asynchronous API client](#connections-asynchronous). ::: lancedb.db.DBConnection +::: lancedb.Session + +## Namespaces (Synchronous) + +A namespace-backed connection resolves tables through a +[Lance namespace](https://lancedb.github.io/lance-namespace/) service instead of +listing a storage directory. + +::: lancedb.connect_namespace + +::: lancedb.namespace.LanceNamespaceDBConnection + ## Tables (Synchronous) ::: lancedb.table.Table @@ -34,8 +46,12 @@ is also an [asynchronous API client](#connections-asynchronous). ::: lancedb.table.FragmentSummaryStats +::: lancedb.table.TableStatistics + ::: lancedb.table.Tags +::: lancedb.table.Branches + ## Expressions Type-safe expression builder for filters and projections. Use these instead @@ -62,29 +78,46 @@ of raw SQL strings with [where][lancedb.query.LanceQueryBuilder.where] and ::: lancedb.query.LanceHybridQueryBuilder +::: lancedb.query.LanceEmptyQueryBuilder + +::: lancedb.query.LanceTakeQueryBuilder + +## Full text queries + +Structured full text queries can be passed to +[Table.search][lancedb.table.Table.search] or +[AsyncTable.search][lancedb.table.AsyncTable.search] in place of a query string, +and combined with [BooleanQuery][lancedb.query.BooleanQuery]. + +::: lancedb.query.FullTextQuery + +::: lancedb.query.MatchQuery + +::: lancedb.query.PhraseQuery + +::: lancedb.query.BoostQuery + +::: lancedb.query.MultiMatchQuery + +::: lancedb.query.BooleanQuery + +::: lancedb.query.FullTextOperator + +::: lancedb.query.Occur + ## Embeddings -::: lancedb.embeddings.registry.EmbeddingFunctionRegistry - -::: lancedb.embeddings.base.EmbeddingFunctionConfig - -::: lancedb.embeddings.base.EmbeddingFunction - -::: lancedb.embeddings.base.TextEmbeddingFunction - -::: lancedb.embeddings.sentence_transformers.SentenceTransformerEmbeddings - -::: lancedb.embeddings.openai.OpenAIEmbeddings - -::: lancedb.embeddings.open_clip.OpenClipEmbeddings +::: lancedb.embeddings + options: + show_root_heading: false + show_root_toc_entry: false ## Remote configuration -::: lancedb.remote.ClientConfig - -::: lancedb.remote.TimeoutConfig - -::: lancedb.remote.RetryConfig +::: lancedb.remote + options: + show_root_heading: false + show_root_toc_entry: false ## Context @@ -122,7 +155,22 @@ tokens = list(lancedb.tokenize("acme makes searchable data", custom_stop_words=["acme"])) ``` -::: lancedb.index.FTS +::: lancedb.tokenize + +::: lancedb.FtsToken + +## Blobs + +Blob columns store large binary values out of line so they can be read lazily +instead of being materialized with the rest of the row. + +::: lancedb.blob + +::: lancedb.BlobType + +::: lancedb._blob.BlobFile + options: + show_root_full_path: false ## Utilities @@ -130,6 +178,14 @@ tokens = list(lancedb.tokenize("acme makes searchable data", ::: lancedb.merge.LanceMergeInsertBuilder +::: lancedb.otel.instrument_lancedb_metrics + +## Exceptions + +::: lancedb.exceptions.MissingValueError + +::: lancedb.exceptions.MissingColumnError + ## Integrations ## Pydantic @@ -138,19 +194,30 @@ tokens = list(lancedb.tokenize("acme makes searchable data", ::: lancedb.pydantic.vector +::: lancedb.pydantic.Vector + +::: lancedb.pydantic.MultiVector + ::: lancedb.pydantic.LanceModel +## PyTorch + +::: lancedb.streaming.StreamingDataset + +::: lancedb.permutation.permutation_builder + +::: lancedb.permutation.PermutationBuilder + +::: lancedb.permutation.Permutation + +::: lancedb.permutation.Transforms + ## Reranking -::: lancedb.rerankers.linear_combination.LinearCombinationReranker - -::: lancedb.rerankers.cohere.CohereReranker - -::: lancedb.rerankers.colbert.ColbertReranker - -::: lancedb.rerankers.cross_encoder.CrossEncoderReranker - -::: lancedb.rerankers.openai.OpenaiReranker +::: lancedb.rerankers + options: + show_root_heading: false + show_root_toc_entry: false ## Connections (Asynchronous) @@ -161,6 +228,12 @@ can be used to create, list, or open tables. ::: lancedb.db.AsyncConnection +## Namespaces (Asynchronous) + +::: lancedb.connect_namespace_async + +::: lancedb.namespace.AsyncLanceNamespaceDBConnection + ## Tables (Asynchronous) Table hold your actual data as a collection of records / rows. @@ -169,32 +242,20 @@ Table hold your actual data as a collection of records / rows. ::: lancedb.table.AsyncTags +::: lancedb.table.AsyncBranches + ## Indices (Asynchronous) Indices can be created on a table to speed up queries. This section lists the indices that LanceDb supports. -::: lancedb.index.BTree - -::: lancedb.index.Bitmap - -::: lancedb.index.LabelList - -::: lancedb.index.FTS - -::: lancedb.index.IvfPq - -::: lancedb.index.HnswPq - -::: lancedb.index.HnswSq - -::: lancedb.index.IvfFlat - -::: lancedb.index.IvfSq - -::: lancedb.index.IvfRq - -::: lancedb.index.HnswFlat +::: lancedb.index + options: + show_root_heading: false + show_root_toc_entry: false + # `lang_mapping` is defined in the module rather than imported, so it is + # picked up despite not being in `__all__`. It is an internal lookup table. + filters: ["!^_", "!^lang_mapping$"] ::: lancedb.table.IndexStatistics @@ -222,3 +283,7 @@ rows nearest to a query vector and can be created with the ::: lancedb.query.AsyncHybridQuery options: inherited_members: true + +::: lancedb.query.AsyncTakeQuery + options: + inherited_members: true diff --git a/nodejs/CONTRIBUTING.md b/nodejs/CONTRIBUTING.md index d784b9f75..2faa8cd37 100644 --- a/nodejs/CONTRIBUTING.md +++ b/nodejs/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing to LanceDB Typescript This document outlines the process for contributing to LanceDB Typescript. -For general contribution guidelines, see [CONTRIBUTING.md](../CONTRIBUTING.md). +For general contribution guidelines, see [CONTRIBUTING.md](https://github.com/lancedb/lancedb/blob/main/CONTRIBUTING.md). ## Project layout diff --git a/python/python/lancedb/db.py b/python/python/lancedb/db.py index 75ff7239d..90b8dc75b 100644 --- a/python/python/lancedb/db.py +++ b/python/python/lancedb/db.py @@ -404,7 +404,7 @@ class DBConnection(EnforceOverrides): Data is converted to Arrow before being written to disk. For maximum control over how data is saved, either provide the PyArrow schema to - convert to or else provide a [PyArrow Table](pyarrow.Table) directly. + convert to or else provide a [PyArrow Table][pyarrow.Table] directly. >>> import pyarrow as pa >>> custom_schema = pa.schema([ @@ -1570,7 +1570,7 @@ class AsyncConnection(object): Data is converted to Arrow before being written to disk. For maximum control over how data is saved, either provide the PyArrow schema to - convert to or else provide a [PyArrow Table](pyarrow.Table) directly. + convert to or else provide a [PyArrow Table][pyarrow.Table] directly. >>> import pyarrow as pa >>> custom_schema = pa.schema([ diff --git a/python/python/lancedb/embeddings/__init__.py b/python/python/lancedb/embeddings/__init__.py index f70aa57b2..4a39b7900 100644 --- a/python/python/lancedb/embeddings/__init__.py +++ b/python/python/lancedb/embeddings/__init__.py @@ -21,3 +21,32 @@ from .watsonx import WatsonxEmbeddings from .voyageai import VoyageAIEmbeddingFunction from .colpali import ColPaliEmbeddings from .siglip import SigLipEmbeddings + +# The API reference renders this package with a single mkdocstrings directive, +# which only picks up names listed here. New embedding functions must be added +# to both the imports above and this list, or they will silently go undocumented. +__all__ = [ + "EmbeddingFunction", + "EmbeddingFunctionConfig", + "TextEmbeddingFunction", + "EmbeddingFunctionRegistry", + "get_registry", + "register", + "SentenceTransformerEmbeddings", + "OpenAIEmbeddings", + "OpenClipEmbeddings", + "BedRockText", + "CohereEmbeddingFunction", + "GeminiText", + "GteEmbeddings", + "InstructorEmbeddingFunction", + "JinaEmbeddings", + "OllamaEmbeddings", + "TransformersEmbeddingFunction", + "ColbertEmbeddings", + "VoyageAIEmbeddingFunction", + "WatsonxEmbeddings", + "ColPaliEmbeddings", + "ImageBindEmbeddings", + "SigLipEmbeddings", +] diff --git a/python/python/lancedb/embeddings/bedrock.py b/python/python/lancedb/embeddings/bedrock.py index ffa44f0c8..dc2badceb 100644 --- a/python/python/lancedb/embeddings/bedrock.py +++ b/python/python/lancedb/embeddings/bedrock.py @@ -21,20 +21,20 @@ class BedRockText(TextEmbeddingFunction): """ Parameters ---------- - name: str, default "amazon.titan-embed-text-v1" + name : str, default "amazon.titan-embed-text-v1" The model ID of the bedrock model to use. Supported models for are: - amazon.titan-embed-text-v1 - cohere.embed-english-v3 - cohere.embed-multilingual-v3 - region: str, default "us-east-1" + region : str, default "us-east-1" Optional name of the AWS Region in which the service should be called. - profile_name: str, default None + profile_name : str, default None Optional name of the AWS profile to use for calling the Bedrock service. If not specified, the default profile will be used. - assumed_role: str, default None + assumed_role : str, default None Optional ARN of an AWS IAM role to assume for calling the Bedrock service. If not specified, the current active credentials will be used. - role_session_name: str, default "lancedb-embeddings" + role_session_name : str, default "lancedb-embeddings" Optional name of the AWS IAM role session to use for calling the Bedrock service. If not specified, "lancedb-embeddings" name will be used. diff --git a/python/python/lancedb/embeddings/cohere.py b/python/python/lancedb/embeddings/cohere.py index 8769ddaaa..8fc460f9a 100644 --- a/python/python/lancedb/embeddings/cohere.py +++ b/python/python/lancedb/embeddings/cohere.py @@ -22,7 +22,7 @@ class CohereEmbeddingFunction(TextEmbeddingFunction): Parameters ---------- - name: str, default "embed-multilingual-v2.0" + name : str, default "embed-multilingual-v2.0" The name of the model to use. List of acceptable models: * embed-english-v3.0 @@ -33,12 +33,14 @@ class CohereEmbeddingFunction(TextEmbeddingFunction): * embed-english-light-v2.0 * embed-multilingual-v2.0 - source_input_type: str, default "search_document" + source_input_type : str, default "search_document" The input type for the source column in the database - query_input_type: str, default "search_query" + query_input_type : str, default "search_query" The input type for the query column in the database + Notes + ----- Cohere supports following input types: | Input Type | Description | diff --git a/python/python/lancedb/embeddings/colpali.py b/python/python/lancedb/embeddings/colpali.py index 4b614bce3..7232d32ce 100644 --- a/python/python/lancedb/embeddings/colpali.py +++ b/python/python/lancedb/embeddings/colpali.py @@ -44,7 +44,7 @@ class ColPaliEmbeddings(EmbeddingFunction): The token pooling strategy to use, by default "hierarchical". - "hierarchical": Progressively pools tokens to reduce sequence length. - "lambda": A simpler pooling that uses a custom `pooling_func`. - pooling_func: typing.Callable, optional + pooling_func : typing.Callable, optional A function to use for pooling when `pooling_strategy` is "lambda". pool_factor : int Factor to reduce sequence length if token pooling is enabled (default 2). @@ -52,7 +52,7 @@ class ColPaliEmbeddings(EmbeddingFunction): Quantization configuration for the model. (default None, bitsandbytes needed) batch_size : int Batch size for processing inputs (default 2). - offload_folder: str, optional + offload_folder : str, optional Folder to offload model weights if using CPU offloading (default None). This is useful for large models that do not fit in memory. """ diff --git a/python/python/lancedb/embeddings/gemini_text.py b/python/python/lancedb/embeddings/gemini_text.py index 1102e92d1..32f2d4d04 100644 --- a/python/python/lancedb/embeddings/gemini_text.py +++ b/python/python/lancedb/embeddings/gemini_text.py @@ -48,16 +48,16 @@ class GeminiText(TextEmbeddingFunction): Parameters ---------- - name: str, default "gemini-embedding-001" + name : str, default "gemini-embedding-001" The name of the model to use. Supported models include: - "gemini-embedding-001" (768 dimensions) Note: The legacy "models/embedding-001" format is also supported but "gemini-embedding-001" is recommended. - query_task_type: str, default "retrieval_query" + query_task_type : str, default "retrieval_query" Sets the task type for the queries. - source_task_type: str, default "retrieval_document" + source_task_type : str, default "retrieval_document" Sets the task type for ingestion. Examples diff --git a/python/python/lancedb/embeddings/gte.py b/python/python/lancedb/embeddings/gte.py index 4f547a30b..9bad4b54f 100644 --- a/python/python/lancedb/embeddings/gte.py +++ b/python/python/lancedb/embeddings/gte.py @@ -26,13 +26,13 @@ class GteEmbeddings(TextEmbeddingFunction): Parameters ---------- - name: str, default "thenlper/gte-large" + name : str, default "thenlper/gte-large" The name of the model to use. - device: str, default "cpu" + device : str, default "cpu" Sets the device type for the model. - normalize: str, default "True" + normalize : str, default "True" Controls normalize param in encode function for the transformer. - mlx: bool, default False + mlx : bool, default False Controls which model to use. False for gte-large,True for the mlx version. Examples diff --git a/python/python/lancedb/embeddings/instructor.py b/python/python/lancedb/embeddings/instructor.py index a694fd382..675a0139c 100644 --- a/python/python/lancedb/embeddings/instructor.py +++ b/python/python/lancedb/embeddings/instructor.py @@ -35,23 +35,23 @@ class InstructorEmbeddingFunction(TextEmbeddingFunction): Parameters ---------- - name: str + name : str The name of the model to use. Available models are listed at https://github.com/xlang-ai/instructor-embedding#model-list; The default model is hkunlp/instructor-base - batch_size: int, default 32 + batch_size : int, default 32 The batch size to use when generating embeddings - device: str, default "cpu" + device : str, default "cpu" The device to use when generating embeddings - show_progress_bar: bool, default True + show_progress_bar : bool, default True Whether to show a progress bar when generating embeddings - normalize_embeddings: bool, default True + normalize_embeddings : bool, default True Whether to normalize the embeddings - quantize: bool, default False + quantize : bool, default False Whether to quantize the model - source_instruction: str, default "represent the document for retrieval" + source_instruction : str, default "represent the document for retrieval" The instruction for the source column - query_instruction: str, default "represent the document for retrieving the most + query_instruction : str, default "represent the document for retrieving the most similar documents" The instruction for the query diff --git a/python/python/lancedb/embeddings/jinaai.py b/python/python/lancedb/embeddings/jinaai.py index 720e80e85..9656f041f 100644 --- a/python/python/lancedb/embeddings/jinaai.py +++ b/python/python/lancedb/embeddings/jinaai.py @@ -40,10 +40,10 @@ class JinaEmbeddings(EmbeddingFunction): Parameters ---------- - name: str, default "jina-clip-v1". Note that some models support both image + name : str, default "jina-clip-v1". Note that some models support both image and text embeddings and some just text embedding - api_key: str, default None + api_key : str, default None The api key to access Jina API. If you pass None, you can set JINA_API_KEY environment variable diff --git a/python/python/lancedb/embeddings/sentence_transformers.py b/python/python/lancedb/embeddings/sentence_transformers.py index 30d283cf7..8be02e9f7 100644 --- a/python/python/lancedb/embeddings/sentence_transformers.py +++ b/python/python/lancedb/embeddings/sentence_transformers.py @@ -21,13 +21,13 @@ class SentenceTransformerEmbeddings(TextEmbeddingFunction): Parameters ---------- - name: str, default "all-MiniLM-L6-v2" + name : str, default "all-MiniLM-L6-v2" The name of the model to use. - device: str, default "cpu" + device : str, default "cpu" The device to use for the model - normalize: bool, default True + normalize : bool, default True Whether to normalize the embeddings - trust_remote_code: bool, default True + trust_remote_code : bool, default True Whether to trust the remote code """ diff --git a/python/python/lancedb/embeddings/voyageai.py b/python/python/lancedb/embeddings/voyageai.py index 3cf70d332..4a721c998 100644 --- a/python/python/lancedb/embeddings/voyageai.py +++ b/python/python/lancedb/embeddings/voyageai.py @@ -167,7 +167,7 @@ class VoyageAIEmbeddingFunction(EmbeddingFunction): Parameters ---------- - name: str + name : str The name of the model to use. List of acceptable models: * voyage-4 (1024 dims, general-purpose and multilingual retrieval) @@ -185,7 +185,7 @@ class VoyageAIEmbeddingFunction(EmbeddingFunction): * voyage-law-2 * voyage-code-2 - output_dimension: int, optional + output_dimension : int, optional The output dimension for models that support flexible dimensions. Currently only voyage-multimodal-3.5 supports this feature. Valid options: 256, 512, 1024 (default), 2048. diff --git a/python/python/lancedb/index.py b/python/python/lancedb/index.py index 5ced0600f..aa7846892 100644 --- a/python/python/lancedb/index.py +++ b/python/python/lancedb/index.py @@ -219,7 +219,7 @@ class HnswPq: distance has a range of (-∞, ∞). If the vectors are normalized (i.e. their l2 norm is 1), then dot distance is equivalent to the cosine distance. - num_partitions, default sqrt(num_rows) + num_partitions: int, default sqrt(num_rows) The number of IVF partitions to create. @@ -228,7 +228,7 @@ class HnswPq: will require too much memory. Each partition becomes its own HNSW graph, so setting this value higher reduces the peak memory use of training. - num_sub_vectors, default is vector dimension / 16 + num_sub_vectors: int, default is vector dimension / 16 Number of sub-vectors of PQ. @@ -244,13 +244,13 @@ class HnswPq: If the dimension is not visible by 8 then we use 1 subvector. This is not ideal and will likely result in poor performance. - num_bits: int, default 8 + num_bits: int, default 8 Number of bits to encode each sub-vector. This value controls how much the sub-vectors are compressed. The more bits the more accurate the index but the slower search. Only 4 and 8 are supported. - max_iterations, default 50 + max_iterations: int, default 50 Max iterations to train kmeans. @@ -263,7 +263,7 @@ class HnswPq: those cases it is unlikely that setting this larger will lead to the index converging anyways. - sample_rate, default 256 + sample_rate: int, default 256 The rate used to calculate the number of training vectors for kmeans. @@ -279,14 +279,14 @@ class HnswPq: Increasing this value might improve the quality of the index but in most cases the default should be sufficient. - m, default 20 + m: int, default 20 The number of neighbors to select for each vector in the HNSW graph. This value controls the tradeoff between search speed and accuracy. The higher the value the more accurate the search but the slower it will be. - ef_construction, default 300 + ef_construction: int, default 300 The number of candidates to evaluate during the construction of the HNSW graph. @@ -297,7 +297,7 @@ class HnswPq: This value should be set to a value that is not less than `ef` in the search phase. - target_partition_size, default is 1,048,576 + target_partition_size: int, default is 1,048,576 The target size of each partition. @@ -351,7 +351,7 @@ class HnswSq: distance has a range of (-∞, ∞). If the vectors are normalized (i.e. their l2 norm is 1), then dot distance is equivalent to the cosine distance. - num_partitions, default sqrt(num_rows) + num_partitions: int, default sqrt(num_rows) The number of IVF partitions to create. @@ -360,7 +360,7 @@ class HnswSq: will require too much memory. Each partition becomes its own HNSW graph, so setting this value higher reduces the peak memory use of training. - max_iterations, default 50 + max_iterations: int, default 50 Max iterations to train kmeans. @@ -373,7 +373,7 @@ class HnswSq: In those cases it is unlikely that setting this larger will lead to the index converging anyways. - sample_rate, default 256 + sample_rate: int, default 256 The rate used to calculate the number of training vectors for kmeans. @@ -389,14 +389,14 @@ class HnswSq: Increasing this value might improve the quality of the index but in most cases the default should be sufficient. - m, default 20 + m: int, default 20 The number of neighbors to select for each vector in the HNSW graph. This value controls the tradeoff between search speed and accuracy. The higher the value the more accurate the search but the slower it will be. - ef_construction, default 300 + ef_construction: int, default 300 The number of candidates to evaluate during the construction of the HNSW graph. @@ -407,7 +407,7 @@ class HnswSq: This value should be set to a value that is not less than `ef` in the search phase. - target_partition_size, default is 1,048,576 + target_partition_size: int, default is 1,048,576 The target size of each partition. @@ -460,7 +460,7 @@ class HnswFlat: distance has a range of (-∞, ∞). If the vectors are normalized (i.e. their l2 norm is 1), then dot distance is equivalent to the cosine distance. - num_partitions, default sqrt(num_rows) + num_partitions: int, default sqrt(num_rows) The number of IVF partitions to create. @@ -470,18 +470,18 @@ class HnswFlat: graph, so setting this value higher reduces the peak memory use of training. - max_iterations, default 50 + max_iterations: int, default 50 Max iterations to train kmeans. When training an IVF index we use kmeans to calculate the partitions. This parameter controls how many iterations of kmeans to run. - sample_rate, default 256 + sample_rate: int, default 256 The rate used to calculate the number of training vectors for kmeans. - m, default 20 + m: int, default 20 The number of neighbors to select for each vector in the HNSW graph. @@ -489,7 +489,7 @@ class HnswFlat: The higher the value the more accurate the search but the slower it will be. - ef_construction, default 300 + ef_construction: int, default 300 The number of candidates to evaluate during the construction of the HNSW graph. @@ -501,7 +501,7 @@ class HnswFlat: than 500. This value should be set to a value that is not less than `ef` in the search phase. - target_partition_size, default is 1,048,576 + target_partition_size: int, default is 1,048,576 The target size of each partition. """ @@ -605,7 +605,7 @@ class IvfFlat: The default value is 256. - target_partition_size, default is 8192 + target_partition_size: int, default is 8192 The target size of each partition. @@ -769,7 +769,7 @@ class IvfPq: The default value is 256. - target_partition_size, default is 8192 + target_partition_size: int, default is 8192 The target size of each partition. @@ -830,7 +830,7 @@ class IvfRq: sample_rate: int, default 256 Controls the number of training vectors: sample_rate * num_partitions. - target_partition_size, default is 8192 + target_partition_size: int, default is 8192 Target size of each partition. """ @@ -845,6 +845,9 @@ class IvfRq: accelerator: Optional[str] = None +# The API reference renders this module with a single mkdocstrings directive, +# which only picks up names listed here. New public names must be added to this +# list, or they will silently go undocumented. __all__ = [ "BTree", "IvfPq", diff --git a/python/python/lancedb/permutation.py b/python/python/lancedb/permutation.py index 8aa764c4b..b3d95c352 100644 --- a/python/python/lancedb/permutation.py +++ b/python/python/lancedb/permutation.py @@ -438,7 +438,8 @@ class Permutation: _reader: Optional[PermutationReader] = None, ): """ - Internal constructor. Use [from_tables](#from_tables) instead. + Internal constructor. Use + [from_tables][lancedb.permutation.Permutation.from_tables] instead. """ assert base_table is not None, "base_table is required" assert selection is not None, "selection is required" @@ -985,8 +986,9 @@ class Permutation: types. Conversion of strings, lists, and structs will require creating python objects and this is not zero-copy. - For custom formatting, use [with_transform](#with_transform) which overrides - this method. + For custom formatting, use + [with_transform][lancedb.permutation.Permutation.with_transform] which + overrides this method. """ assert format is not None, "format is required" if format == "python": @@ -1061,7 +1063,8 @@ class Permutation: Note: this method returns a new permutation and does not modify `self` It is provided for compatibility with the huggingface Dataset API. - Use [with_skip](#with_skip) instead to avoid confusion. + Use [with_skip][lancedb.permutation.Permutation.with_skip] instead to + avoid confusion. """ return self.with_skip(skip) @@ -1084,7 +1087,8 @@ class Permutation: Note: this method returns a new permutation and does not modify `self` It is provided for compatibility with the huggingface Dataset API. - Use [with_take](#with_take) instead to avoid confusion. + Use [with_take][lancedb.permutation.Permutation.with_take] instead to + avoid confusion. """ return self.with_take(limit) @@ -1107,7 +1111,8 @@ class Permutation: Note: this method returns a new permutation and does not modify `self` It is provided for compatibility with the huggingface Dataset API. - Use [with_repeat](#with_repeat) instead to avoid confusion. + Use [with_repeat][lancedb.permutation.Permutation.with_repeat] instead + to avoid confusion. """ return self.with_repeat(times) diff --git a/python/python/lancedb/query.py b/python/python/lancedb/query.py index 181eb9349..60c5d0c68 100644 --- a/python/python/lancedb/query.py +++ b/python/python/lancedb/query.py @@ -650,7 +650,8 @@ class Query(pydantic.BaseModel): distance_type : Optional[str] the distance type to use for vector search - This can be l2 (default), cosine and dot. See [metric definitions][search] for + This can be l2 (default), cosine and dot. See + [metric definitions](https://lancedb.com/docs/search/vector-search/) for more details. If this is not a vector search this will be None. @@ -663,8 +664,9 @@ class Query(pydantic.BaseModel): - A higher number makes search more accurate but also slower. - - See discussion in [Querying an ANN Index][querying-an-ann-index] for - tuning advice. + - See discussion in + [Querying an ANN Index](https://lancedb.com/docs/indexing/) + for tuning advice. Will be None if this is not a vector search. refine_factor : Optional[int] @@ -672,8 +674,9 @@ class Query(pydantic.BaseModel): - A higher number makes search more accurate but also slower. - - See discussion in [Querying an ANN Index][querying-an-ann-index] for - tuning advice. + - See discussion in + [Querying an ANN Index](https://lancedb.com/docs/indexing/) + for tuning advice. Will be None if this is not a vector search. lower_bound : Optional[float] @@ -1647,8 +1650,8 @@ class LanceVectorQueryBuilder(LanceQueryBuilder): Higher values will yield better recall (more likely to find vectors if they exist) at the expense of latency. - See discussion in [Querying an ANN Index][querying-an-ann-index] for - tuning advice. + See discussion in [Querying an ANN Index](https://lancedb.com/docs/indexing/) + for tuning advice. This method sets both the minimum and maximum number of probes to the same value. See `minimum_nprobes` and `maximum_nprobes` for more fine-grained @@ -1748,8 +1751,8 @@ class LanceVectorQueryBuilder(LanceQueryBuilder): As an example, a refine factor of 2 will sample 2x as many vectors as requested, re-ranks them, and returns the top half most relevant results. - See discussion in [Querying an ANN Index][querying-an-ann-index] for - tuning advice. + See discussion in [Querying an ANN Index](https://lancedb.com/docs/indexing/) + for tuning advice. Parameters ---------- @@ -3373,8 +3376,9 @@ class AsyncQuery(AsyncStandardQuery): are various ANN search parameters that will let you fine tune your recall accuracy vs search latency. - Vector searches always have a [limit][]. If `limit` has not been called then - a default `limit` of 10 will be used. + Vector searches always have a + [limit][lancedb.query.AsyncVectorQuery.limit]. If `limit` has not been + called then a default `limit` of 10 will be used. Typically, a single vector is passed in as the query. However, you can also pass in multiple vectors. When multiple vectors are passed in, if the vector @@ -3505,8 +3509,9 @@ class AsyncFTSQuery(AsyncStandardQuery): are various ANN search parameters that will let you fine tune your recall accuracy vs search latency. - Hybrid searches always have a [limit][]. If `limit` has not been called then - a default `limit` of 10 will be used. + Hybrid searches always have a + [limit][lancedb.query.AsyncHybridQuery.limit]. If `limit` has not been + called then a default `limit` of 10 will be used. Typically, a single vector is passed in as the query. However, you can also pass in multiple vectors. This can be useful if you want to find the nearest diff --git a/python/python/lancedb/remote/__init__.py b/python/python/lancedb/remote/__init__.py index a6ef55eb5..1f255b991 100644 --- a/python/python/lancedb/remote/__init__.py +++ b/python/python/lancedb/remote/__init__.py @@ -11,6 +11,9 @@ from lancedb import __version__ from .header import HeaderProvider from .oauth import OAuthConfig, OAuthFlowType +# The API reference renders this module with a single mkdocstrings directive, +# which only picks up names listed here. New public names must be added to this +# list, or they will silently go undocumented. __all__ = [ "TimeoutConfig", "RetryConfig", diff --git a/python/python/lancedb/remote/errors.py b/python/python/lancedb/remote/errors.py index 2f2dad9b4..5aa13a9c7 100644 --- a/python/python/lancedb/remote/errors.py +++ b/python/python/lancedb/remote/errors.py @@ -53,9 +53,9 @@ class RetryError(LanceDBClientError): """An error that occurs when the client has exceeded the maximum number of retries. The retry strategy can be adjusted by setting the - [retry_config](lancedb.remote.ClientConfig.retry_config) in the client + [retry_config][lancedb.remote.ClientConfig.retry_config] in the client configuration. This is passed in the `client_config` argument of - [connect](lancedb.connect) and [connect_async](lancedb.connect_async). + [connect][lancedb.connect] and [connect_async][lancedb.connect_async]. The __cause__ attribute of this exception will be the last exception that caused the retry to fail. It will be an diff --git a/python/python/lancedb/remote/table.py b/python/python/lancedb/remote/table.py index 20a952c53..268e0d5a4 100644 --- a/python/python/lancedb/remote/table.py +++ b/python/python/lancedb/remote/table.py @@ -581,8 +581,9 @@ class RemoteTable(Table): progress: Optional[Union[bool, Callable, Any]] = None, write_parallelism: Optional[int] = None, ) -> AddResult: - """Add more data to the [Table](Table). It has the same API signature as - the OSS version. + """Add more data to the [Table][lancedb.table.Table]. + + It has the same API signature as the OSS version. Parameters ---------- @@ -642,7 +643,8 @@ class RemoteTable(Table): fast_search: bool = False, ) -> LanceVectorQueryBuilder: """Create a search query to find the nearest neighbors - of the given query vector. We currently support [vector search][search] + of the given query vector. We currently support + [vector search](https://lancedb.com/docs/search/vector-search/) All query options are defined in [LanceVectorQueryBuilder][lancedb.query.LanceVectorQueryBuilder]. diff --git a/python/python/lancedb/rerankers/__init__.py b/python/python/lancedb/rerankers/__init__.py index 86b0ac884..aef2d331e 100644 --- a/python/python/lancedb/rerankers/__init__.py +++ b/python/python/lancedb/rerankers/__init__.py @@ -14,6 +14,9 @@ from .answerdotai import AnswerdotaiRerankers from .voyageai import VoyageAIReranker from .watsonx import WatsonxReranker +# The API reference renders this module with a single mkdocstrings directive, +# which only picks up names listed here. New public names must be added to this +# list, or they will silently go undocumented. __all__ = [ "Reranker", "CrossEncoderReranker", diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index 26d78a427..399b075f0 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -1211,7 +1211,7 @@ class Table(ABC): progress: Optional[Union[bool, Callable, Any]] = None, write_parallelism: Optional[int] = None, ) -> AddResult: - """Add more data to the [Table](Table). + """Add more data to the [Table][lancedb.table.Table]. Parameters ---------- @@ -1343,8 +1343,8 @@ class Table(ABC): fts_columns: Optional[Union[str, List[str]]] = None, ) -> LanceQueryBuilder: """Create a search query to find the nearest neighbors - of the given query vector. We currently support [vector search][search] - and [full-text search][experimental-full-text-search]. + of the given query vector. We currently support [vector search](https://lancedb.com/docs/search/vector-search/) + and [full-text search](https://lancedb.com/docs/search/full-text-search/). All query options are defined in [LanceQueryBuilder][lancedb.query.LanceQueryBuilder]. @@ -1780,7 +1780,7 @@ class Table(ABC): for faster reads. Arguments are passed onto Lance's - [compact_files][lance.dataset.DatasetOptimizer.compact_files]. + `lance.dataset.DatasetOptimizer.compact_files`. For most cases, the default should be fine. See Also @@ -1834,6 +1834,8 @@ class Table(ABC): retrain: bool, default False This parameter is no longer used and is deprecated. + Notes + ----- The frequency an application should call optimize is based on the frequency of data modifications. If data is frequently added, deleted, or updated then optimize should be run frequently. A good rule of thumb is to run optimize if @@ -1988,15 +1990,14 @@ class Table(ABC): change permanent you can use the `[Self::restore]` method. Any operation that modifies the table will fail while the table is in a checked - out state. + out state. To return the table to a normal state use + `[Self::checkout_latest]`. Parameters ---------- version: int | str, The version to check out. A version number (`int`) or a tag (`str`) can be provided. - - To return the table to a normal state use `[Self::checkout_latest]` """ @abstractmethod @@ -3383,8 +3384,8 @@ class LanceTable(Table): fts_columns: Optional[Union[str, List[str]]] = None, ) -> LanceQueryBuilder: """Create a search query to find the nearest neighbors - of the given query vector. We currently support [vector search][search] - and [full-text search][search]. + of the given query vector. We currently support [vector search](https://lancedb.com/docs/search/vector-search/) + and [full-text search](https://lancedb.com/docs/search/full-text-search/). Examples -------- @@ -3414,8 +3415,9 @@ class LanceTable(Table): - *default None*. Acceptable types are: list, np.ndarray, PIL.Image.Image - - If None then the select/[where][sql]/limit clauses are applied - to filter the table + - If None then the + select/[where][lancedb.query.LanceQueryBuilder.where]/limit clauses + are applied to filter the table vector_column_name: str, optional The name of the vector column to search. @@ -3809,6 +3811,8 @@ class LanceTable(Table): retrain: bool, default False This parameter is no longer used and is deprecated. + Notes + ----- The frequency an application should call optimize is based on the frequency of data modifications. If data is frequently added, deleted, or updated then optimize should be run frequently. A good rule of thumb is to run optimize if @@ -4687,7 +4691,7 @@ class AsyncTable: Parameters ---------- **kwargs - Forwarded to [`lance.dataset`][lance.dataset]. + Forwarded to `lance.dataset`. Returns ------- @@ -5006,7 +5010,7 @@ class AsyncTable: progress: Optional[Union[bool, Callable, Any]] = None, write_parallelism: Optional[int] = None, ) -> AddResult: - """Add more data to the [Table](Table). + """Add more data to the [AsyncTable][lancedb.table.AsyncTable]. Parameters ---------- @@ -5208,8 +5212,8 @@ class AsyncTable: fts_columns: Optional[Union[str, List[str]]] = None, ) -> Union[AsyncHybridQuery, AsyncFTSQuery, AsyncVectorQuery]: """Create a search query to find the nearest neighbors - of the given query vector. We currently support [vector search][search] - and [full-text search][experimental-full-text-search]. + of the given query vector. We currently support [vector search](https://lancedb.com/docs/search/vector-search/) + and [full-text search](https://lancedb.com/docs/search/full-text-search/). All query options are defined in [AsyncQuery][lancedb.query.AsyncQuery]. @@ -5770,15 +5774,14 @@ class AsyncTable: change permanent you can use the `[Self::restore]` method. Any operation that modifies the table will fail while the table is in a checked - out state. + out state. To return the table to a normal state use + `[Self::checkout_latest]`. Parameters ---------- version: int | str, The version to check out. A version number (`int`) or a tag (`str`) can be provided. - - To return the table to a normal state use `[Self::checkout_latest]` """ try: await self._inner.checkout(version) @@ -5962,6 +5965,8 @@ class AsyncTable: retrain: bool, default False This parameter is no longer used and is deprecated. + Notes + ----- The frequency an application should call optimize is based on the frequency of data modifications. If data is frequently added, deleted, or updated then optimize should be run frequently. A good rule of thumb is to run optimize if @@ -6342,6 +6347,8 @@ class Branches: dry_run: bool, default False When True, only preview. When False, attempt the merge. + Notes + ----- A rejected merge returns ``status="rejected"`` instead of raising. """ return LOOP.run(self._table.branches.merge(from_branch, dry_run)) From dd2b11eda26aa87f0af4eeb036c25fa8dbb16951 Mon Sep 17 00:00:00 2001 From: Cohen Karnell Date: Thu, 30 Jul 2026 21:32:31 -0500 Subject: [PATCH 22/29] fix(python): log when storage_options is ignored in RemoteDBConnection.open_table (#3743) `RemoteDBConnection.open_table` accepts `storage_options` and never uses it: ```python def open_table( self, name: str, *, namespace_path: Optional[List[str]] = None, storage_options: Optional[Dict[str, str]] = None, index_cache_size: Optional[int] = None, ... ) -> Table: ... if index_cache_size is not None: logging.info("index_cache_size is ignored in LanceDb Cloud ...") table = LOOP.run(self._conn.open_table(name, namespace_path=namespace_path)) ``` The value is never passed down and never mentioned. `index_cache_size` is ignored on Cloud in the same way, but it says so. I checked this at runtime on 0.34.0, not just by reading it: swapping the inner connection for a recorder, `open_table("t", storage_options={...})` hands the layer below `['namespace_path']` and nothing else, no log record is emitted, and the same probe shows `index_cache_size` producing its message as expected. This adds the matching log line, so the two ignored parameters behave the same way. `ruff check` and `ruff format --check` are clean on the file. A note on severity. This is not a security hole and nothing is exposed. Someone passing credentials there gets silence instead of an error, and finds out later. One thing I am unsure about, and it changes the fix. I have assumed per-table storage options are meaningless on Cloud, which is what the `index_cache_size` line next to it implies about managed storage. If they are supposed to work, then the right change is to pass them through to `self._conn.open_table` instead and this patch is the wrong one. Happy to redo it that way. I did not check whether `create_table` or the async connection have the same gap. --- python/python/lancedb/remote/db.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/python/lancedb/remote/db.py b/python/python/lancedb/remote/db.py index 201be33d5..3171c3638 100644 --- a/python/python/lancedb/remote/db.py +++ b/python/python/lancedb/remote/db.py @@ -415,6 +415,11 @@ class RemoteDBConnection(DBConnection): if namespace_path is None: namespace_path = [] + if storage_options is not None: + logging.info( + "storage_options is ignored in LanceDb Cloud" + " (storage is managed; set storage_options on connect() instead)" + ) if index_cache_size is not None: logging.info( "index_cache_size is ignored in LanceDb Cloud" From a6418b6cb9907a08ba11eee3316ec93f7bfe5104 Mon Sep 17 00:00:00 2001 From: Wyatt Alt Date: Fri, 31 Jul 2026 07:32:28 -0700 Subject: [PATCH 23/29] feat: create_index returns a Job handle (#3742) IndexBuilder::execute now returns a Job with wait and cancel methods. Local tables build the index synchronously and return an already-done job. Remote tables read the job id the server returns from create_index and track it through the /v1/jobs API: wait polls describe until the job reaches a terminal state and cancel posts a cancellation. Servers that return no job id yield a done job, so behavior against older servers is unchanged. The job id is not exposed on the handle. The Python and TypeScript bindings keep their current signatures and discard the handle; exposing Job there is left to follow-ups. --------- Co-authored-by: Claude Fable 5 --- docs/src/js/classes/Job.md | 64 ++++ docs/src/js/classes/Table.md | 23 ++ docs/src/js/globals.md | 1 + nodejs/__test__/table.test.ts | 6 +- nodejs/lancedb/index.ts | 2 +- nodejs/lancedb/table.ts | 28 ++ nodejs/src/job.rs | 44 +++ nodejs/src/lib.rs | 1 + nodejs/src/table.rs | 33 ++ python/python/lancedb/__init__.py | 3 + python/python/lancedb/_lancedb.pyi | 28 ++ python/python/lancedb/exceptions.py | 12 + python/python/lancedb/job.py | 83 +++++ python/python/lancedb/remote/table.py | 29 ++ python/python/lancedb/table.py | 87 +++++ python/python/tests/test_index.py | 9 + python/python/tests/test_remote_db.py | 115 +++++++ python/python/tests/test_table.py | 9 + python/src/error.rs | 12 + python/src/job.rs | 46 +++ python/src/lib.rs | 2 + python/src/table.rs | 31 ++ rust/lancedb/src/error.rs | 50 ++- rust/lancedb/src/index.rs | 10 +- rust/lancedb/src/job.rs | 158 +++++++++ rust/lancedb/src/lib.rs | 4 +- rust/lancedb/src/remote.rs | 1 + rust/lancedb/src/remote/job.rs | 147 ++++++++ rust/lancedb/src/remote/table.rs | 442 +++++++++++++++++++------ rust/lancedb/src/table.rs | 37 +-- rust/lancedb/src/table/create_index.rs | 246 ++++++++++++++ 31 files changed, 1636 insertions(+), 127 deletions(-) create mode 100644 docs/src/js/classes/Job.md create mode 100644 nodejs/src/job.rs create mode 100644 python/python/lancedb/job.py create mode 100644 python/src/job.rs create mode 100644 rust/lancedb/src/job.rs create mode 100644 rust/lancedb/src/remote/job.rs diff --git a/docs/src/js/classes/Job.md b/docs/src/js/classes/Job.md new file mode 100644 index 000000000..c46b9651f --- /dev/null +++ b/docs/src/js/classes/Job.md @@ -0,0 +1,64 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / Job + +# Class: Job + +A handle to an operation that may still be running. + +## Constructors + +### new Job() + +```ts +new Job(): Job +``` + +#### Returns + +[`Job`](Job.md) + +## Accessors + +### id + +```ts +get id(): null | string +``` + +Identifies the operation on the server that is running it. Operations +that run in this process have no server id. The value is opaque. + +#### Returns + +`null` \| `string` + +## Methods + +### cancel() + +```ts +cancel(): Promise +``` + +Request cancellation. Cancelling a finished operation is a no-op. + +#### Returns + +`Promise`<`void`> + +*** + +### wait() + +```ts +wait(): Promise +``` + +Wait until the operation reaches a terminal state. + +#### Returns + +`Promise`<`void`> diff --git a/docs/src/js/classes/Table.md b/docs/src/js/classes/Table.md index 05636657e..11fca32d0 100644 --- a/docs/src/js/classes/Table.md +++ b/docs/src/js/classes/Table.md @@ -295,6 +295,29 @@ await table.createIndex("my_float_col"); *** +### createIndexAsync() + +```ts +abstract createIndexAsync(column, options?): Promise +``` + +Create an index, returning a handle to the indexing job. + +The job may already be complete when returned; callers must not assume +the index exists until [Job.wait](Job.md#wait) resolves. + +#### Parameters + +* **column**: `string` + +* **options?**: `Partial`<[`IndexOptions`](../interfaces/IndexOptions.md)> + +#### Returns + +`Promise`<[`Job`](Job.md)> + +*** + ### currentBranch() ```ts diff --git a/docs/src/js/globals.md b/docs/src/js/globals.md index c214a5f13..323a24a49 100644 --- a/docs/src/js/globals.md +++ b/docs/src/js/globals.md @@ -25,6 +25,7 @@ - [Connection](classes/Connection.md) - [HeaderProvider](classes/HeaderProvider.md) - [Index](classes/Index.md) +- [Job](classes/Job.md) - [MakeArrowTableOptions](classes/MakeArrowTableOptions.md) - [MatchQuery](classes/MatchQuery.md) - [MergeInsertBuilder](classes/MergeInsertBuilder.md) diff --git a/nodejs/__test__/table.test.ts b/nodejs/__test__/table.test.ts index 41025047e..4cad365af 100644 --- a/nodejs/__test__/table.test.ts +++ b/nodejs/__test__/table.test.ts @@ -851,7 +851,11 @@ describe("When creating an index", () => { afterEach(() => tmpDir.removeCallback()); it("should create a vector index on vector columns", async () => { - await tbl.createIndex("vec"); + const job = await tbl.createIndexAsync("vec"); + expect(job.id).toBeNull(); + await job.wait(); + // Cancelling a job that already finished succeeds and does nothing. + await job.cancel(); // check index directory const indexDir = path.join(tmpDir.name, "test.lance", "_indices"); diff --git a/nodejs/lancedb/index.ts b/nodejs/lancedb/index.ts index 63a4b08d0..2ce031458 100644 --- a/nodejs/lancedb/index.ts +++ b/nodejs/lancedb/index.ts @@ -85,7 +85,7 @@ export { RenameTableOptions, } from "./connection"; -export { Session } from "./native.js"; +export { Job, Session } from "./native.js"; export { ExecutableQuery, diff --git a/nodejs/lancedb/table.ts b/nodejs/lancedb/table.ts index d814e3e55..3359a2643 100644 --- a/nodejs/lancedb/table.ts +++ b/nodejs/lancedb/table.ts @@ -30,6 +30,7 @@ import { DropColumnsResult, IndexConfig, IndexStatistics, + Job, Branches as NativeBranches, OptimizeStats, TableStatistics, @@ -358,6 +359,17 @@ export abstract class Table { options?: Partial, ): Promise; + /** + * Create an index, returning a handle to the indexing job. + * + * The job may already be complete when returned; callers must not assume + * the index exists until {@link Job.wait} resolves. + */ + abstract createIndexAsync( + column: string, + options?: Partial, + ): Promise; + /** * Drop an index from the table. * @@ -940,6 +952,22 @@ export class LocalTable extends Table { ); } + async createIndexAsync( + column: string, + options?: Partial, + ): Promise { + // biome-ignore lint/suspicious/noExplicitAny: skip + const nativeIndex = (options?.config as any)?.inner; + return await this.inner.createIndexAsync( + nativeIndex, + column, + options?.replace, + options?.waitTimeoutSeconds, + options?.name, + options?.train, + ); + } + async dropIndex(name: string): Promise { await this.inner.dropIndex(name); } diff --git a/nodejs/src/job.rs b/nodejs/src/job.rs new file mode 100644 index 000000000..52d082beb --- /dev/null +++ b/nodejs/src/job.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The LanceDB Authors + +use std::sync::Arc; + +use napi_derive::napi; + +use crate::error::NapiErrorExt; + +/// A handle to an operation that may still be running. +#[napi] +pub struct Job { + inner: Arc, +} + +impl Job { + pub(crate) fn new(inner: lancedb::Job) -> Self { + Self { + inner: Arc::new(inner), + } + } +} + +#[napi] +impl Job { + /// Identifies the operation on the server that is running it. Operations + /// that run in this process have no server id. The value is opaque. + #[napi(getter)] + pub fn id(&self) -> Option { + self.inner.id().map(str::to_string) + } + + /// Wait until the operation reaches a terminal state. + #[napi(catch_unwind)] + pub async fn wait(&self) -> napi::Result<()> { + self.inner.wait().await.default_error() + } + + /// Request cancellation. Cancelling a finished operation is a no-op. + #[napi(catch_unwind)] + pub async fn cancel(&self) -> napi::Result<()> { + self.inner.cancel().await.default_error() + } +} diff --git a/nodejs/src/lib.rs b/nodejs/src/lib.rs index 93bb2044a..312b675bd 100644 --- a/nodejs/src/lib.rs +++ b/nodejs/src/lib.rs @@ -11,6 +11,7 @@ mod error; mod header; mod index; mod iterator; +mod job; pub mod merge; pub mod otel; pub mod permutation; diff --git a/nodejs/src/table.rs b/nodejs/src/table.rs index d1489034f..7dd902e19 100644 --- a/nodejs/src/table.rs +++ b/nodejs/src/table.rs @@ -168,6 +168,39 @@ impl Table { builder.execute().await.default_error() } + #[napi(catch_unwind)] + pub async fn create_index_async( + &self, + index: Option<&Index>, + column: String, + replace: Option, + wait_timeout_s: Option, + name: Option, + train: Option, + ) -> napi::Result { + let lancedb_index = if let Some(index) = index { + index.consume()? + } else { + lancedb::index::Index::Auto + }; + let mut builder = self.inner_ref()?.create_index(&[column], lancedb_index); + if let Some(replace) = replace { + builder = builder.replace(replace); + } + if let Some(timeout) = wait_timeout_s { + builder = + builder.wait_timeout(std::time::Duration::from_secs(timeout.try_into().unwrap())); + } + if let Some(name) = name { + builder = builder.name(name); + } + if let Some(train) = train { + builder = builder.train(train); + } + let job = builder.execute_async().await.default_error()?; + Ok(crate::job::Job::new(job)) + } + #[napi(catch_unwind)] pub async fn drop_index(&self, index_name: String) -> napi::Result<()> { self.inner_ref()? diff --git a/python/python/lancedb/__init__.py b/python/python/lancedb/__init__.py index 71349b8a5..235049f97 100644 --- a/python/python/lancedb/__init__.py +++ b/python/python/lancedb/__init__.py @@ -20,6 +20,7 @@ from .remote import ClientConfig from .remote.db import RemoteDBConnection from .expr import Expr, col, lit, func from .schema import blob, vector, BlobType +from .job import AsyncJob, Job from .table import AsyncTable, Table from .types import BaseTokenizerType from ._lancedb import Session @@ -500,6 +501,7 @@ __all__ = [ "connect_namespace", "connect_namespace_async", "AsyncConnection", + "AsyncJob", "AsyncLanceNamespaceDBConnection", "AsyncTable", "FtsToken", @@ -513,6 +515,7 @@ __all__ = [ "BlobType", "vector", "DBConnection", + "Job", "LanceDBConnection", "LanceNamespaceDBConnection", "RemoteDBConnection", diff --git a/python/python/lancedb/_lancedb.pyi b/python/python/lancedb/_lancedb.pyi index 67325e10b..f5c59c152 100644 --- a/python/python/lancedb/_lancedb.pyi +++ b/python/python/lancedb/_lancedb.pyi @@ -209,6 +209,12 @@ class BlobFile: def read_range(self, offset: int, length: int) -> bytes: ... def read_up_to(self, length: int) -> bytes: ... +class Job: + @property + def id(self) -> Optional[str]: ... + async def wait(self) -> None: ... + async def cancel(self) -> None: ... + class Table: def name(self) -> str: ... def __repr__(self) -> str: ... @@ -248,6 +254,28 @@ class Table: name: Optional[str], train: Optional[bool], ): ... + async def create_index_async( + self, + column: str, + index: Union[ + IvfFlat, + IvfSq, + IvfPq, + HnswPq, + HnswSq, + HnswFlat, + BTree, + Bitmap, + LabelList, + Fm, + FTS, + ], + replace: Optional[bool], + wait_timeout: Optional[object], + *, + name: Optional[str], + train: Optional[bool], + ) -> Job: ... async def list_versions(self) -> List[Dict[str, Any]]: ... async def version(self) -> int: ... async def checkout(self, version: Union[int, str]): ... diff --git a/python/python/lancedb/exceptions.py b/python/python/lancedb/exceptions.py index 0ac0971e3..daa98ee6e 100644 --- a/python/python/lancedb/exceptions.py +++ b/python/python/lancedb/exceptions.py @@ -23,3 +23,15 @@ class MissingColumnError(KeyError): return ( f"Error: Column '{self.column_name}' does not exist in the DataFrame object" ) + + +class JobFailedError(RuntimeError): + """Exception raised when an asynchronous job reaches the failed state.""" + + pass + + +class JobCancelledError(RuntimeError): + """Exception raised when an asynchronous job was cancelled.""" + + pass diff --git a/python/python/lancedb/job.py b/python/python/lancedb/job.py new file mode 100644 index 000000000..82911119b --- /dev/null +++ b/python/python/lancedb/job.py @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright The LanceDB Authors + +"""Handles to operations a server may run asynchronously.""" + +import asyncio +from datetime import timedelta +from typing import Optional + +from lancedb.background_loop import LOOP + +from . import _lancedb + + +class AsyncJob: + """A handle to an operation that may still be running. + + The operation may already be complete when the handle is created. + """ + + def __init__(self, inner: Optional["_lancedb.Job"]): + self._inner = inner + + @property + def id(self) -> Optional[str]: + """Identifies the operation on the server that is running it. + + Returned for correlating with server logs or the jobs API. Operations + that run in this process have no server id and return `None`. The value + is opaque: parsing it or storing it to resume the job later is not + supported. + """ + return self._inner.id if self._inner is not None else None + + async def wait(self, timeout: Optional[timedelta] = None): + """Wait until the operation reaches a terminal state. + + Raises `JobFailedError` if the operation failed, `JobCancelledError` + if it was cancelled, and `TimeoutError` if `timeout` elapses first. + """ + if self._inner is None: + return + if timeout is None: + await self._inner.wait() + else: + await asyncio.wait_for(self._inner.wait(), timeout.total_seconds()) + + async def cancel(self): + """Request cancellation. Cancelling a finished operation is a no-op.""" + if self._inner is None: + return + await self._inner.cancel() + + +class Job: + """Synchronous counterpart of `AsyncJob`.""" + + def __init__(self, inner: Optional[AsyncJob]): + self._inner = inner + + @property + def id(self) -> Optional[str]: + """Identifies the operation on the server that is running it. + + See :attr:`AsyncJob.id`. + """ + return self._inner.id if self._inner is not None else None + + def wait(self, timeout: Optional[timedelta] = None): + """Block until the operation reaches a terminal state. + + Raises `JobFailedError` if the operation failed, `JobCancelledError` + if it was cancelled, and `TimeoutError` if `timeout` elapses first. + """ + if self._inner is None: + return + LOOP.run(self._inner.wait(timeout)) + + def cancel(self): + """Request cancellation. Cancelling a finished operation is a no-op.""" + if self._inner is None: + return + LOOP.run(self._inner.cancel()) diff --git a/python/python/lancedb/remote/table.py b/python/python/lancedb/remote/table.py index 268e0d5a4..acc2f4c9d 100644 --- a/python/python/lancedb/remote/table.py +++ b/python/python/lancedb/remote/table.py @@ -48,6 +48,7 @@ from lancedb.index import ( IvfSq, LabelList, ) +from lancedb.job import Job from lancedb.remote.db import LOOP from lancedb.table import IndexConfigType, KNOWN_METRICS import pyarrow as pa @@ -541,6 +542,34 @@ class RemoteTable(Table): ) ) + def create_index_async( + self, + column: str, + *, + config: IndexConfigType, + replace: Optional[bool] = None, + wait_timeout: Optional[timedelta] = None, + name: Optional[str] = None, + train: bool = True, + ) -> Job: + """Create an index, returning a handle to the indexing job. + + The job may already be complete when returned; callers must not assume + the index exists until :meth:`Job.wait` returns. + """ + return Job( + LOOP.run( + self._table.create_index_async( + column, + replace=replace, + config=config, + wait_timeout=wait_timeout, + name=name, + train=train, + ) + ) + ) + def _is_legacy_create_index_call( self, first_arg: str, diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index 399b075f0..ea0f97621 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -40,6 +40,7 @@ from ._blob import ( from .types import BlobMode from lancedb.arrow import peek_reader from lancedb.background_loop import LOOP, embedding_executor +from lancedb.job import AsyncJob, Job from .dependencies import ( _check_for_hugging_face, _check_for_lance, @@ -977,6 +978,24 @@ class Table(ABC): """ raise NotImplementedError + def create_index_async( + self, + column: str, + *, + config: IndexConfigType, + replace: Optional[bool] = None, + wait_timeout: Optional[timedelta] = None, + name: Optional[str] = None, + train: bool = True, + ) -> Job: + """Create an index, returning a handle to the indexing job. + + Takes the same arguments as :meth:`create_index`. The job may already + be complete when returned; callers must not assume the index exists + until :meth:`Job.wait` returns. + """ + raise NotImplementedError + def drop_index(self, name: str) -> None: """ Drop an index from the table. @@ -2780,6 +2799,34 @@ class LanceTable(Table): ) ) + def create_index_async( + self, + column: str, + *, + config: IndexConfigType, + replace: Optional[bool] = None, + wait_timeout: Optional[timedelta] = None, + name: Optional[str] = None, + train: bool = True, + ) -> Job: + """Create an index, returning a handle to the indexing job. + + The job may already be complete when returned; callers must not assume + the index exists until :meth:`Job.wait` returns. + """ + return Job( + LOOP.run( + self._table.create_index_async( + column, + replace=replace, + config=config, + wait_timeout=wait_timeout, + name=name, + train=train, + ) + ) + ) + def _is_legacy_create_index_call( self, first_arg: str, @@ -4867,6 +4914,46 @@ class AsyncTable: ) raise e + async def create_index_async( + self, + column: str, + *, + replace: Optional[bool] = None, + config: Optional[ + Union[ + IvfFlat, + IvfPq, + IvfRq, + HnswPq, + HnswSq, + HnswFlat, + BTree, + Bitmap, + LabelList, + Fm, + FTS, + ] + ] = None, + wait_timeout: Optional[timedelta] = None, + name: Optional[str] = None, + train: bool = True, + ) -> AsyncJob: + """Create an index, returning a handle to the indexing job. + + Takes the same arguments as :meth:`create_index`. The job may already + be complete when returned; callers must not assume the index exists + until :meth:`AsyncJob.wait` resolves. + """ + job = await self._inner.create_index_async( + column, + index=config, + replace=replace, + wait_timeout=wait_timeout, + name=name, + train=train, + ) + return AsyncJob(job) + async def drop_index(self, name: str) -> None: """ Drop an index from the table. diff --git a/python/python/tests/test_index.py b/python/python/tests/test_index.py index 65a244c5b..1cf2c733c 100644 --- a/python/python/tests/test_index.py +++ b/python/python/tests/test_index.py @@ -84,6 +84,15 @@ async def binary_table(db_async): ) +@pytest.mark.asyncio +async def test_create_index_async_returns_done_job(some_table: AsyncTable): + job = await some_table.create_index_async("id", config=BTree()) + assert job.id is None + await job.wait() + assert len(await some_table.list_indices()) == 1 + await job.cancel() + + @pytest.mark.asyncio async def test_create_scalar_index(some_table: AsyncTable): # Can create diff --git a/python/python/tests/test_remote_db.py b/python/python/tests/test_remote_db.py index dd46628f1..9bb162b15 100644 --- a/python/python/tests/test_remote_db.py +++ b/python/python/tests/test_remote_db.py @@ -812,6 +812,121 @@ def test_table_create_indices(): table.drop_index("custom_fts_idx") +def test_remote_create_index_async_returns_job(): + from lancedb.index import BTree + + describe_calls = [] + + def handler(request): + content_len = int(request.headers.get("Content-Length", 0)) + body = request.rfile.read(content_len) if content_len > 0 else b"" + if request.path == "/v1/table/test/create_index/": + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write(b'{"job_id": "job-1"}') + elif request.path == "/v1/jobs/describe": + assert json.loads(body)["job_id"] == "job-1" + describe_calls.append(1) + state = "IN_PROGRESS" if len(describe_calls) == 1 else "DONE" + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write( + json.dumps(dict(job_id="job-1", job_state=state)).encode() + ) + elif request.path == "/v1/jobs/cancel": + assert json.loads(body)["job_id"] == "job-1" + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write(b"{}") + elif request.path == "/v1/table/test/create/?mode=create": + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write(b"{}") + elif request.path == "/v1/table/test/describe/": + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write( + json.dumps( + dict( + version=1, + schema=dict( + fields=[ + dict(name="id", type={"type": "int64"}, nullable=False), + ] + ), + ) + ).encode() + ) + else: + request.send_response(404) + request.end_headers() + + with mock_lancedb_connection(handler) as db: + table = db.create_table("test", [{"id": 1}]) + job = table.create_index_async("id", config=BTree()) + assert job.id == "job-1" + job.wait(timeout=timedelta(seconds=30)) + assert len(describe_calls) == 2 + job.cancel() + + +def test_remote_job_wait_raises_on_failure(): + from lancedb.exceptions import JobFailedError + from lancedb.index import BTree + + def handler(request): + content_len = int(request.headers.get("Content-Length", 0)) + body = request.rfile.read(content_len) if content_len > 0 else b"" + if request.path == "/v1/table/test/create_index/": + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write(b'{"job_id": "job-2"}') + elif request.path == "/v1/jobs/describe": + assert json.loads(body)["job_id"] == "job-2" + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write( + json.dumps(dict(job_id="job-2", job_state="FAILED")).encode() + ) + elif request.path == "/v1/table/test/create/?mode=create": + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write(b"{}") + elif request.path == "/v1/table/test/describe/": + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write( + json.dumps( + dict( + version=1, + schema=dict( + fields=[ + dict(name="id", type={"type": "int64"}, nullable=False), + ] + ), + ) + ).encode() + ) + else: + request.send_response(404) + request.end_headers() + + with mock_lancedb_connection(handler) as db: + table = db.create_table("test", [{"id": 1}]) + job = table.create_index_async("id", config=BTree()) + with pytest.raises(JobFailedError, match="job-2"): + job.wait() + + def test_remote_create_index_new_api(): received_requests = [] diff --git a/python/python/tests/test_table.py b/python/python/tests/test_table.py index 2a34084cf..a355cf101 100644 --- a/python/python/tests/test_table.py +++ b/python/python/tests/test_table.py @@ -1402,6 +1402,15 @@ async def test_async_open_table_with_branch_version(tmp_path): assert await pinned.count_rows() == 4 # writable again +def test_create_index_async_returns_done_job(mem_db: DBConnection): + table = mem_db.create_table("job_test", [{"id": i} for i in range(10)]) + job = table.create_index_async("id", config=BTree()) + assert job.id is None + job.wait() + assert len(table.list_indices()) == 1 + job.cancel() + + @patch("lancedb.table.AsyncTable.create_index") def test_create_index_method(mock_create_index, mem_db: DBConnection): table = mem_db.create_table( diff --git a/python/src/error.rs b/python/src/error.rs index 062a1dc96..b66afe47b 100644 --- a/python/src/error.rs +++ b/python/src/error.rs @@ -102,6 +102,18 @@ impl PythonErrorExt for std::result::Result { err.setattr(intern!(py, "__cause__"), cause_err)?; Err(PyErr::from_value(err)) }), + LanceError::JobFailed { .. } => Python::attach(|py| { + let cls = py + .import(intern!(py, "lancedb.exceptions"))? + .getattr(intern!(py, "JobFailedError"))?; + Err(PyErr::from_value(cls.call1((err.to_string(),))?)) + }), + LanceError::JobCancelled { .. } => Python::attach(|py| { + let cls = py + .import(intern!(py, "lancedb.exceptions"))? + .getattr(intern!(py, "JobCancelledError"))?; + Err(PyErr::from_value(cls.call1((err.to_string(),))?)) + }), _ => self.runtime_error(), }, } diff --git a/python/src/job.rs b/python/src/job.rs new file mode 100644 index 000000000..83303ddea --- /dev/null +++ b/python/src/job.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The LanceDB Authors + +use std::sync::Arc; + +use crate::runtime::future_into_py; +use pyo3::{Bound, PyAny, PyRef, PyResult, pyclass, pymethods}; + +use crate::error::PythonErrorExt; + +#[pyclass] +pub struct Job { + inner: Arc, +} + +impl Job { + pub(crate) fn new(inner: lancedb::Job) -> Self { + Self { + inner: Arc::new(inner), + } + } +} + +#[pymethods] +impl Job { + #[getter] + pub fn id(&self) -> Option { + self.inner.id().map(str::to_string) + } + + pub fn wait(self_: PyRef<'_, Self>) -> PyResult> { + let inner = self_.inner.clone(); + future_into_py(self_.py(), async move { + inner.wait().await.infer_error()?; + Ok(()) + }) + } + + pub fn cancel(self_: PyRef<'_, Self>) -> PyResult> { + let inner = self_.inner.clone(); + future_into_py(self_.py(), async move { + inner.cancel().await.infer_error()?; + Ok(()) + }) + } +} diff --git a/python/src/lib.rs b/python/src/lib.rs index d480f9613..6c9f62df9 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -25,6 +25,7 @@ pub mod error; pub mod expr; pub mod header; pub mod index; +pub mod job; pub mod namespace; pub mod oauth; pub mod otel; @@ -44,6 +45,7 @@ pub fn _lancedb(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::
()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/python/src/table.rs b/python/src/table.rs index 84ba91f39..a4e50c183 100644 --- a/python/src/table.rs +++ b/python/src/table.rs @@ -805,6 +805,37 @@ impl Table { }) } + #[pyo3(signature = (column, index=None, replace=None, wait_timeout=None, *, name=None, train=None))] + pub fn create_index_async<'a>( + self_: PyRef<'a, Self>, + column: String, + index: Option>, + replace: Option, + wait_timeout: Option>, + name: Option, + train: Option, + ) -> PyResult> { + let index = extract_index_params(&index)?; + let timeout = wait_timeout.map(|t| t.extract::().unwrap()); + let mut op = self_ + .inner_ref()? + .create_index_with_timeout(&[column], index, timeout); + if let Some(replace) = replace { + op = op.replace(replace); + } + if let Some(name) = name { + op = op.name(name); + } + if let Some(train) = train { + op = op.train(train); + } + + future_into_py(self_.py(), async move { + let job = op.execute_async().await.infer_error()?; + Ok(crate::job::Job::new(job)) + }) + } + pub fn drop_index(self_: PyRef<'_, Self>, index_name: String) -> PyResult> { let inner = self_.inner_ref()?.clone(); future_into_py(self_.py(), async move { diff --git a/rust/lancedb/src/error.rs b/rust/lancedb/src/error.rs index 9e4dd1f8c..f6f596f3d 100644 --- a/rust/lancedb/src/error.rs +++ b/rust/lancedb/src/error.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright The LanceDB Authors -use std::sync::PoisonError; +use std::fmt::{self, Display, Formatter}; +use std::sync::{Arc, PoisonError}; use arrow_schema::ArrowError; use datafusion_common::DataFusionError; @@ -9,6 +10,46 @@ use snafu::Snafu; pub(crate) type BoxError = Box; +/// Why a job failed, to whatever precision the backend provides. +/// +/// A job run in this process carries the error it failed with in [`Self::source`]. +/// A job run remotely carries whatever the server reported, which older servers +/// do not report at all. Every field is absent rather than invented when the +/// backend does not supply it. +#[derive(Debug, Clone, Default)] +pub struct JobFailure { + /// The stage the job was in, when known. + pub phase: Option, + /// A human-readable reason, when known. + pub message: Option, + /// Whether a retry could clear the failure, when known. + pub retryable: Option, + /// The error the job failed with, when it ran in this process. + pub source: Option>, +} + +impl JobFailure { + /// A failure whose only known detail is the error that caused it. + pub(crate) fn from_source(source: Arc) -> Self { + Self { + message: Some(source.to_string()), + source: Some(source), + ..Default::default() + } + } +} + +impl Display for JobFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match (&self.message, &self.phase) { + (Some(message), Some(phase)) => write!(f, ": {message} (in {phase})"), + (Some(message), None) => write!(f, ": {message}"), + (None, Some(phase)) => write!(f, " in {phase}"), + (None, None) => Ok(()), + } + } +} + #[derive(Debug, Snafu)] #[snafu(visibility(pub(crate)))] pub enum Error { @@ -44,6 +85,13 @@ pub enum Error { Runtime { message: String }, #[snafu(display("Timeout error: {message}"))] Timeout { message: String }, + #[snafu(display("Job{} failed{failure}", job_id.as_ref().map(|id| format!(" {id}")).unwrap_or_default()))] + JobFailed { + job_id: Option, + failure: JobFailure, + }, + #[snafu(display("Job{} was cancelled", job_id.as_ref().map(|id| format!(" {id}")).unwrap_or_default()))] + JobCancelled { job_id: Option }, // 3rd party / external errors #[snafu(display("object_store error: {source}"))] diff --git a/rust/lancedb/src/index.rs b/rust/lancedb/src/index.rs index 308a64ca1..c693dc056 100644 --- a/rust/lancedb/src/index.rs +++ b/rust/lancedb/src/index.rs @@ -10,7 +10,7 @@ use std::time::Duration; use vector::IvfFlatIndexBuilder; use crate::index::vector::IvfRqIndexBuilder; -use crate::{DistanceType, Error, Result, table::BaseTable}; +use crate::{DistanceType, Error, Result, job::Job, table::BaseTable}; use self::{ scalar::{BTreeIndexBuilder, BitmapIndexBuilder, FmIndexBuilder, LabelListIndexBuilder}, @@ -305,6 +305,14 @@ impl IndexBuilder { pub async fn execute(self) -> Result<()> { self.parent.clone().create_index(self).await } + + /// Creates the index, returning a [`Job`] tracking the operation. + /// + /// The job may already be complete when returned, and callers must not + /// assume the index exists until [`Job::wait`] resolves. + pub async fn execute_async(self) -> Result { + self.parent.clone().create_index_async(self).await + } } #[derive(Debug, Clone, PartialEq, Deserialize)] diff --git a/rust/lancedb/src/job.rs b/rust/lancedb/src/job.rs new file mode 100644 index 000000000..4c2c76d95 --- /dev/null +++ b/rust/lancedb/src/job.rs @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The LanceDB Authors + +//! Handles to operations a server may run asynchronously. + +use std::sync::Arc; + +use async_trait::async_trait; +use tokio::sync::watch; +use tokio::task::{AbortHandle, JoinHandle}; + +use crate::error::{Error, JobFailure, Result}; + +/// Backend-specific tracking for an asynchronous operation. +#[async_trait] +pub(crate) trait JobHandle: Send + Sync { + /// Server-assigned id, when the backend has one. + fn id(&self) -> Option<&str> { + None + } + async fn wait(&self) -> Result<()>; + async fn cancel(&self) -> Result<()>; +} + +/// A handle to an operation that may still be running. +/// +/// The operation may already be complete when the handle is created. +pub struct Job { + handle: Option>, +} + +impl std::fmt::Debug for Job { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Job") + .field("id", &self.id()) + .field("done", &self.handle.is_none()) + .finish() + } +} + +impl Job { + /// A job whose operation finished before the handle was created. + pub(crate) fn new_done() -> Self { + Self { handle: None } + } + + pub(crate) fn new(handle: Box) -> Self { + Self { + handle: Some(handle), + } + } + + /// A job running as a task in this process. + pub(crate) fn spawned(task: JoinHandle>) -> Self { + Self::new(Box::new(SpawnedJob::new(task))) + } + + /// Identifies the operation on the server that is running it. + /// + /// Returned for correlating with server logs or the jobs API. Operations + /// that run in this process have no server id and return `None`. The + /// value is opaque: parsing it or storing it to resume the job later is + /// not supported. + pub fn id(&self) -> Option<&str> { + self.handle.as_ref().and_then(|handle| handle.id()) + } + + /// Waits until the operation reaches a terminal state. + /// + /// Returns [`crate::Error::JobFailed`] if the operation failed and + /// [`crate::Error::JobCancelled`] if it was cancelled. + pub async fn wait(&self) -> Result<()> { + match &self.handle { + None => Ok(()), + Some(handle) => handle.wait().await, + } + } + + /// Requests cancellation of the operation. + /// + /// Cancelling an operation that already finished is a no-op. + pub async fn cancel(&self) -> Result<()> { + match &self.handle { + None => Ok(()), + Some(handle) => handle.cancel().await, + } + } +} + +/// How an in-process operation ended. Cloneable so every waiter can be given +/// the outcome; [`Error`] is not, so failures share one behind an [`Arc`]. +#[derive(Clone)] +enum Outcome { + Succeeded, + Failed(Arc), + Cancelled, +} + +impl Outcome { + fn into_result(self) -> Result<()> { + match self { + Self::Succeeded => Ok(()), + Self::Failed(source) => Err(Error::JobFailed { + job_id: None, + failure: JobFailure::from_source(source), + }), + Self::Cancelled => Err(Error::JobCancelled { job_id: None }), + } + } +} + +/// Tracks an operation running as a task in this process. A second task +/// watches the first so that aborting it still produces an outcome, and so +/// that every caller of `wait` observes the same one. +struct SpawnedJob { + outcome: watch::Receiver>, + abort: AbortHandle, +} + +impl SpawnedJob { + fn new(task: JoinHandle>) -> Self { + let abort = task.abort_handle(); + let (tx, outcome) = watch::channel(None); + tokio::spawn(async move { + let outcome = match task.await { + Ok(Ok(())) => Outcome::Succeeded, + Ok(Err(err)) => Outcome::Failed(Arc::new(err)), + Err(err) if err.is_cancelled() => Outcome::Cancelled, + Err(err) => Outcome::Failed(Arc::new(Error::Runtime { + message: format!("index job task failed: {err}"), + })), + }; + let _ = tx.send(Some(outcome)); + }); + Self { outcome, abort } + } +} + +#[async_trait] +impl JobHandle for SpawnedJob { + async fn wait(&self) -> Result<()> { + let mut outcome = self.outcome.clone(); + let settled = outcome + .wait_for(|outcome| outcome.is_some()) + .await + .map_err(|_| Error::Runtime { + message: "index job outcome was dropped before it completed".to_string(), + })? + .clone() + .expect("wait_for returns once an outcome is set"); + settled.into_result() + } + + async fn cancel(&self) -> Result<()> { + self.abort.abort(); + Ok(()) + } +} diff --git a/rust/lancedb/src/lib.rs b/rust/lancedb/src/lib.rs index 3cdf33615..70d023ccc 100644 --- a/rust/lancedb/src/lib.rs +++ b/rust/lancedb/src/lib.rs @@ -184,6 +184,7 @@ pub mod expr; pub mod index; pub mod io; pub mod ipc; +pub mod job; #[cfg(feature = "metrics-otel")] pub mod metrics_otel; #[cfg(feature = "polars")] @@ -203,7 +204,8 @@ use serde::{Deserialize, Serialize}; pub use blob::{BlobRangeRequest, blob, is_blob}; pub use connection::{ConnectNamespaceBuilder, Connection}; -pub use error::{Error, Result}; +pub use error::{Error, JobFailure, Result}; +pub use job::Job; use lance_index::vector::ApproxMode as LanceApproxMode; use lance_linalg::distance::DistanceType as LanceDistanceType; /// Re-export of the [`metrics`](https://docs.rs/metrics) crate facade. Enable diff --git a/rust/lancedb/src/remote.rs b/rust/lancedb/src/remote.rs index 657ae1a47..4b5f8832f 100644 --- a/rust/lancedb/src/remote.rs +++ b/rust/lancedb/src/remote.rs @@ -8,6 +8,7 @@ pub(crate) mod client; pub(crate) mod db; +pub(crate) mod job; pub mod oauth; mod retry; pub(crate) mod table; diff --git a/rust/lancedb/src/remote/job.rs b/rust/lancedb/src/remote/job.rs new file mode 100644 index 000000000..26bbfbbf9 --- /dev/null +++ b/rust/lancedb/src/remote/job.rs @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The LanceDB Authors + +//! Tracking for server-side jobs through the `/v1/jobs` API. + +use std::time::Duration; + +use async_trait::async_trait; +use tokio::time::sleep; + +use serde::{Deserialize, Deserializer}; + +use crate::error::{Error, JobFailure, Result}; +use crate::job::JobHandle; +use crate::remote::client::{HttpSend, RequestResultExt, RestfulLanceDbClient}; + +/// Delay before the second job-state poll; doubles up to [`MAX_POLL_INTERVAL`]. +const INITIAL_POLL_INTERVAL: Duration = Duration::from_millis(200); +const MAX_POLL_INTERVAL: Duration = Duration::from_secs(5); + +#[derive(Debug, Clone, PartialEq, Eq)] +enum JobState { + InProgress, + Cancelled, + Failed, + Done, + /// A state this client version does not know; treated as still running + /// and reported as-is if the job never settles. + Other(String), +} + +impl<'de> Deserialize<'de> for JobState { + fn deserialize>(deserializer: D) -> std::result::Result { + Ok(Self::from(String::deserialize(deserializer)?.as_str())) + } +} + +impl From<&str> for JobState { + fn from(state: &str) -> Self { + match state { + "IN_PROGRESS" => Self::InProgress, + "CANCELLED" => Self::Cancelled, + "FAILED" => Self::Failed, + "DONE" => Self::Done, + other => Self::Other(other.to_string()), + } + } +} + +/// The server's account of why a job failed. Absent from older servers, which +/// report only the terminal state. +#[derive(Deserialize)] +struct ReportedFailure { + phase: String, + message: String, + retryable: bool, +} + +#[derive(Deserialize)] +struct DescribeJobResponse { + job_state: JobState, + #[serde(default)] + failure: Option, +} + +pub struct RemoteJob { + client: RestfulLanceDbClient, + job_id: String, +} + +impl RemoteJob { + pub fn new(client: RestfulLanceDbClient, job_id: String) -> Self { + Self { client, job_id } + } + + /// One `/v1/jobs/describe` round trip. + async fn describe(&self) -> Result { + let request = self + .client + .post("/v1/jobs/describe") + .json(&serde_json::json!({ "job_id": self.job_id })); + let (request_id, response) = self.client.send(request).await?; + let response = self.client.check_response(&request_id, response).await?; + let body = response.text().await.err_to_http(request_id.clone())?; + let description: DescribeJobResponse = + serde_json::from_str(&body).map_err(|e| Error::Http { + source: format!("failed to parse job description: {}", e).into(), + request_id, + status_code: None, + })?; + Ok(description) + } +} + +#[async_trait] +impl JobHandle for RemoteJob { + fn id(&self) -> Option<&str> { + Some(&self.job_id) + } + + async fn wait(&self) -> Result<()> { + let mut interval = INITIAL_POLL_INTERVAL; + loop { + let description = self.describe().await?; + match description.job_state { + JobState::Done => return Ok(()), + JobState::Failed => { + return Err(Error::JobFailed { + job_id: Some(self.job_id.clone()), + failure: description + .failure + .map(|reported| JobFailure { + phase: Some(reported.phase), + message: Some(reported.message), + retryable: Some(reported.retryable), + source: None, + }) + .unwrap_or_default(), + }); + } + JobState::Cancelled => { + return Err(Error::JobCancelled { + job_id: Some(self.job_id.clone()), + }); + } + JobState::InProgress => {} + JobState::Other(ref state) => { + log::debug!("job {} is in unrecognized state {state}", self.job_id) + } + } + sleep(interval).await; + interval = (interval * 2).min(MAX_POLL_INTERVAL); + } + } + + async fn cancel(&self) -> Result<()> { + let request = self + .client + .post("/v1/jobs/cancel") + .json(&serde_json::json!({ "job_id": self.job_id })); + let (request_id, response) = self.client.send(request).await?; + self.client + .check_response(&request_id, response) + .await + .map(|_| ()) + } +} diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index b668f6d67..e9c200521 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -15,7 +15,9 @@ use crate::expr::expr_to_sql_string; use crate::index::Index; use crate::index::IndexStatistics; use crate::index::waiter::wait_for_index; +use crate::job::Job; use crate::query::{QueryFilter, QueryRequest, Select, VectorQueryRequest}; +use crate::remote::job::RemoteJob; use crate::table::AddColumnsResult; use crate::table::AddResult; use crate::table::BranchDiff; @@ -290,6 +292,120 @@ impl std::fmt::Debug for RemoteTable { } impl RemoteTable { + async fn submit_create_index(&self, mut index: IndexBuilder) -> Result> { + self.check_mutable().await?; + let request = self + .client + .post(&format!("/v1/table/{}/create_index/", self.identifier)); + + let column = match index.columns.len() { + 0 => { + return Err(Error::InvalidInput { + message: "No columns specified".into(), + }); + } + 1 => index.columns.pop().unwrap(), + _ => { + return Err(Error::NotSupported { + message: "Indices over multiple columns not yet supported".into(), + }); + } + }; + let schema = self.schema().await?; + let (canonical_column, field) = resolve_arrow_field_path(&schema, &column)?; + let mut body = serde_json::json!({ + "column": canonical_column + }); + + // Add name parameter if provided (for backwards compatibility, only include if Some) + if let Some(ref name) = index.name { + body["name"] = serde_json::Value::String(name.clone()); + } + + // Warn if train=false is specified since it's not meaningful + if !index.train { + log::warn!( + "train=false has no effect remote tables. The index will be created empty and automatically populated in the background." + ); + } + + fn to_json(params: &impl serde::Serialize) -> crate::Result { + serde_json::to_value(params).map_err(|e| Error::InvalidInput { + message: format!("failed to serialize index params {:?}", e), + }) + } + + // Map each Index variant to its wire type name and serializable params. + // Auto is special-cased since it needs schema inspection. + let (index_type_str, params) = match &index.index { + Index::IvfFlat(p) => ("IVF_FLAT", Some(to_json(p)?)), + Index::IvfPq(p) => ("IVF_PQ", Some(to_json(p)?)), + Index::IvfSq(p) => ("IVF_SQ", Some(to_json(p)?)), + Index::IvfHnswSq(p) => ("IVF_HNSW_SQ", Some(to_json(p)?)), + Index::IvfHnswFlat(p) => ("IVF_HNSW_FLAT", Some(to_json(p)?)), + Index::IvfRq(p) => ("IVF_RQ", Some(to_json(p)?)), + Index::BTree(p) => ("BTREE", Some(to_json(p)?)), + Index::Bitmap(p) => ("BITMAP", Some(to_json(p)?)), + Index::LabelList(p) => ("LABEL_LIST", Some(to_json(p)?)), + Index::Fm(p) => ("FM", Some(to_json(p)?)), + Index::FTS(p) => ("FTS", Some(to_json(p)?)), + Index::Auto => { + if supported_vector_data_type(field.data_type()) { + body[METRIC_TYPE_KEY] = + serde_json::Value::String(DistanceType::L2.to_string().to_lowercase()); + ("IVF_PQ", None) + } else if supported_btree_data_type(field.data_type()) { + ("BTREE", None) + } else { + return Err(Error::NotSupported { + message: format!( + "there are no indices supported for the field `{}` with the data type {}", + field.name(), + field.data_type() + ), + }); + } + } + _ => { + return Err(Error::NotSupported { + message: "Index type not supported".into(), + }); + } + }; + + body[INDEX_TYPE_KEY] = index_type_str.into(); + if let Some(params) = params { + for (key, value) in params.as_object().expect("params should be a JSON object") { + body[key] = value.clone(); + } + } + self.apply_branch_body(&mut body); + + let request = request.json(&body); + + let (request_id, response) = self.send(request, true).await?; + + let response = self.check_table_response(&request_id, response).await?; + let job_id = response + .text() + .await + .ok() + .and_then(|body| serde_json::from_str::(&body).ok()) + .and_then(|value| { + value + .get("job_id") + .and_then(|id| id.as_str()) + .map(str::to_string) + }); + + if let Some(wait_timeout) = index.wait_timeout { + let index_name = index.name.unwrap_or_else(|| format!("{}_idx", column)); + self.wait_for_index(&[&index_name], wait_timeout).await?; + } + + Ok(job_id) + } + pub fn new( client: RestfulLanceDbClient, name: String, @@ -2259,107 +2375,15 @@ impl BaseTable for RemoteTable { Ok(delete_response) } - async fn create_index(&self, mut index: IndexBuilder) -> Result<()> { - self.check_mutable().await?; - let request = self - .client - .post(&format!("/v1/table/{}/create_index/", self.identifier)); + async fn create_index(&self, index: IndexBuilder) -> Result<()> { + self.submit_create_index(index).await.map(|_| ()) + } - let column = match index.columns.len() { - 0 => { - return Err(Error::InvalidInput { - message: "No columns specified".into(), - }); - } - 1 => index.columns.pop().unwrap(), - _ => { - return Err(Error::NotSupported { - message: "Indices over multiple columns not yet supported".into(), - }); - } - }; - let schema = self.schema().await?; - let (canonical_column, field) = resolve_arrow_field_path(&schema, &column)?; - let mut body = serde_json::json!({ - "column": canonical_column - }); - - // Add name parameter if provided (for backwards compatibility, only include if Some) - if let Some(ref name) = index.name { - body["name"] = serde_json::Value::String(name.clone()); - } - - // Warn if train=false is specified since it's not meaningful - if !index.train { - log::warn!( - "train=false has no effect remote tables. The index will be created empty and automatically populated in the background." - ); - } - - fn to_json(params: &impl serde::Serialize) -> crate::Result { - serde_json::to_value(params).map_err(|e| Error::InvalidInput { - message: format!("failed to serialize index params {:?}", e), - }) - } - - // Map each Index variant to its wire type name and serializable params. - // Auto is special-cased since it needs schema inspection. - let (index_type_str, params) = match &index.index { - Index::IvfFlat(p) => ("IVF_FLAT", Some(to_json(p)?)), - Index::IvfPq(p) => ("IVF_PQ", Some(to_json(p)?)), - Index::IvfSq(p) => ("IVF_SQ", Some(to_json(p)?)), - Index::IvfHnswSq(p) => ("IVF_HNSW_SQ", Some(to_json(p)?)), - Index::IvfHnswFlat(p) => ("IVF_HNSW_FLAT", Some(to_json(p)?)), - Index::IvfRq(p) => ("IVF_RQ", Some(to_json(p)?)), - Index::BTree(p) => ("BTREE", Some(to_json(p)?)), - Index::Bitmap(p) => ("BITMAP", Some(to_json(p)?)), - Index::LabelList(p) => ("LABEL_LIST", Some(to_json(p)?)), - Index::Fm(p) => ("FM", Some(to_json(p)?)), - Index::FTS(p) => ("FTS", Some(to_json(p)?)), - Index::Auto => { - if supported_vector_data_type(field.data_type()) { - body[METRIC_TYPE_KEY] = - serde_json::Value::String(DistanceType::L2.to_string().to_lowercase()); - ("IVF_PQ", None) - } else if supported_btree_data_type(field.data_type()) { - ("BTREE", None) - } else { - return Err(Error::NotSupported { - message: format!( - "there are no indices supported for the field `{}` with the data type {}", - field.name(), - field.data_type() - ), - }); - } - } - _ => { - return Err(Error::NotSupported { - message: "Index type not supported".into(), - }); - } - }; - - body[INDEX_TYPE_KEY] = index_type_str.into(); - if let Some(params) = params { - for (key, value) in params.as_object().expect("params should be a JSON object") { - body[key] = value.clone(); - } - } - self.apply_branch_body(&mut body); - - let request = request.json(&body); - - let (request_id, response) = self.send(request, true).await?; - - self.check_table_response(&request_id, response).await?; - - if let Some(wait_timeout) = index.wait_timeout { - let index_name = index.name.unwrap_or_else(|| format!("{}_idx", column)); - self.wait_for_index(&[&index_name], wait_timeout).await?; - } - - Ok(()) + async fn create_index_async(&self, index: IndexBuilder) -> Result { + Ok(match self.submit_create_index(index).await? { + Some(job_id) => Job::new(Box::new(RemoteJob::new(self.client.clone(), job_id))), + None => Job::new_done(), + }) } /// Poll until the columns are fully indexed. Will return Error::Timeout if the columns @@ -5154,6 +5178,224 @@ mod tests { } } + #[tokio::test] + async fn test_create_index_returns_job() { + let describe_calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let describe_calls_in_handler = describe_calls.clone(); + let table = Table::new_with_handler("my_table", move |request| { + assert_eq!(request.method(), "POST"); + match request.url().path() { + "/v1/table/my_table/describe/" => { + let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]); + http::Response::builder() + .status(200) + .body(describe_response(&schema)) + .unwrap() + } + "/v1/table/my_table/create_index/" => http::Response::builder() + .status(200) + .body(r#"{"job_id": "job-123"}"#.to_string()) + .unwrap(), + "/v1/jobs/describe" => { + let body = request.body().unwrap().as_bytes().unwrap(); + let body: serde_json::Value = serde_json::from_slice(body).unwrap(); + assert_eq!(body["job_id"], "job-123"); + let state = if describe_calls_in_handler + .fetch_add(1, std::sync::atomic::Ordering::SeqCst) + == 0 + { + "IN_PROGRESS" + } else { + "DONE" + }; + http::Response::builder() + .status(200) + .body(format!( + r#"{{"job_id": "job-123", "job_state": "{state}"}}"# + )) + .unwrap() + } + "/v1/jobs/cancel" => { + let body = request.body().unwrap().as_bytes().unwrap(); + let body: serde_json::Value = serde_json::from_slice(body).unwrap(); + assert_eq!(body["job_id"], "job-123"); + http::Response::builder() + .status(200) + .body("{}".to_string()) + .unwrap() + } + path => panic!("Unexpected path: {}", path), + } + }); + + let job = table + .create_index(&["a"], Index::BTree(Default::default())) + .execute_async() + .await + .unwrap(); + assert_eq!(job.id(), Some("job-123")); + job.wait().await.unwrap(); + assert_eq!(describe_calls.load(std::sync::atomic::Ordering::SeqCst), 2); + job.cancel().await.unwrap(); + } + + /// An unrecognized state is treated as still running, so the client keeps + /// polling rather than reporting a wrong terminal outcome. + #[tokio::test] + async fn test_job_wait_treats_unknown_state_as_running() { + let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let calls_in_handler = calls.clone(); + let table = + Table::new_with_handler("my_table", move |request| match request.url().path() { + "/v1/table/my_table/describe/" => { + let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]); + http::Response::builder() + .status(200) + .body(describe_response(&schema)) + .unwrap() + } + "/v1/table/my_table/create_index/" => http::Response::builder() + .status(200) + .body(r#"{"job_id": "job-unknown"}"#.to_string()) + .unwrap(), + "/v1/jobs/describe" => { + let state = if calls_in_handler + .fetch_add(1, std::sync::atomic::Ordering::SeqCst) + == 0 + { + "SOMETHING_NEW" + } else { + "DONE" + }; + http::Response::builder() + .status(200) + .body(format!( + r#"{{"job_id": "job-unknown", "job_state": "{state}"}}"# + )) + .unwrap() + } + path => panic!("Unexpected path: {}", path), + }); + + let job = table + .create_index(&["a"], Index::BTree(Default::default())) + .execute_async() + .await + .unwrap(); + job.wait().await.unwrap(); + assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 2); + } + + #[tokio::test] + async fn test_job_wait_surfaces_failure() { + let table = + Table::new_with_handler("my_table", move |request| match request.url().path() { + "/v1/table/my_table/describe/" => { + let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]); + http::Response::builder() + .status(200) + .body(describe_response(&schema)) + .unwrap() + } + "/v1/table/my_table/create_index/" => http::Response::builder() + .status(200) + .body(r#"{"job_id": "job-err"}"#.to_string()) + .unwrap(), + "/v1/jobs/describe" => http::Response::builder() + .status(200) + .body(r#"{"job_id": "job-err", "job_state": "FAILED"}"#.to_string()) + .unwrap(), + path => panic!("Unexpected path: {}", path), + }); + + let job = table + .create_index(&["a"], Index::BTree(Default::default())) + .execute_async() + .await + .unwrap(); + let err = job.wait().await.unwrap_err(); + let crate::Error::JobFailed { failure, .. } = &err else { + panic!("expected JobFailed, got {err:?}"); + }; + // The server said only that it failed, so nothing may be invented. + assert!(failure.message.is_none(), "{failure:?}"); + assert!(failure.phase.is_none(), "{failure:?}"); + assert!(failure.retryable.is_none(), "{failure:?}"); + assert_eq!(err.to_string(), "Job job-err failed"); + } + + /// A server that reports why the job failed has that reason surfaced + /// verbatim rather than replaced with a generic message. + #[tokio::test] + async fn test_job_wait_reports_the_server_failure_reason() { + let table = Table::new_with_handler("my_table", move |request| { + match request.url().path() { + "/v1/table/my_table/describe/" => { + let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]); + http::Response::builder() + .status(200) + .body(describe_response(&schema)) + .unwrap() + } + "/v1/table/my_table/create_index/" => http::Response::builder() + .status(200) + .body(r#"{"job_id": "job-err"}"#.to_string()) + .unwrap(), + "/v1/jobs/describe" => http::Response::builder() + .status(200) + .body( + r#"{"job_id": "job-err", "job_state": "FAILED", "failure": {"phase": "commit", "message": "preempted", "retryable": true}}"# + .to_string(), + ) + .unwrap(), + path => panic!("Unexpected path: {}", path), + } + }); + + let job = table + .create_index(&["a"], Index::BTree(Default::default())) + .execute_async() + .await + .unwrap(); + let err = job.wait().await.unwrap_err(); + let crate::Error::JobFailed { failure, .. } = &err else { + panic!("expected JobFailed, got {err:?}"); + }; + assert_eq!(failure.message.as_deref(), Some("preempted")); + assert_eq!(failure.phase.as_deref(), Some("commit")); + assert_eq!(failure.retryable, Some(true)); + assert_eq!(err.to_string(), "Job job-err failed: preempted (in commit)"); + } + + /// Servers that return no job id (e.g. an empty create-index response) + /// yield an already-done job. + #[tokio::test] + async fn test_create_index_without_job_id_is_done() { + let table = + Table::new_with_handler("my_table", move |request| match request.url().path() { + "/v1/table/my_table/describe/" => { + let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]); + http::Response::builder() + .status(200) + .body(describe_response(&schema)) + .unwrap() + } + "/v1/table/my_table/create_index/" => http::Response::builder() + .status(200) + .body("{}".to_string()) + .unwrap(), + path => panic!("Unexpected path: {}", path), + }); + + let job = table + .create_index(&["a"], Index::BTree(Default::default())) + .execute_async() + .await + .unwrap(); + job.wait().await.unwrap(); + job.cancel().await.unwrap(); + } + #[tokio::test] async fn test_create_index_nested_field_paths() { let schema = nested_index_schema(); diff --git a/rust/lancedb/src/table.rs b/rust/lancedb/src/table.rs index e0f12e2ff..1c9d68f4c 100644 --- a/rust/lancedb/src/table.rs +++ b/rust/lancedb/src/table.rs @@ -57,6 +57,7 @@ use crate::error::{Error, Result}; use crate::index::IndexStatistics; use crate::index::{Index, IndexBuilder}; use crate::index::{IndexConfig, IndexStatisticsImpl, IndexType}; +use crate::job::Job; use crate::query::{IntoQueryVector, Query, QueryExecutionOptions, TakeQuery, VectorQuery}; use crate::table::datafusion::insert::InsertExec; use crate::utils::{PatchReadParam, PatchWriteParam, resolve_arrow_field_path}; @@ -613,6 +614,9 @@ pub trait BaseTable: std::fmt::Display + std::fmt::Debug + Send + Sync { async fn update(&self, update: UpdateBuilder) -> Result; /// Create an index on the provided column(s). async fn create_index(&self, index: IndexBuilder) -> Result<()>; + + /// Starts index creation, returning a handle to the resulting job. + async fn create_index_async(&self, index: IndexBuilder) -> Result; /// List the indices on the table. async fn list_indices(&self) -> Result>; /// Drop an index from the table. @@ -3061,29 +3065,18 @@ impl BaseTable for NativeTable { } async fn create_index(&self, opts: IndexBuilder) -> Result<()> { - if opts.columns.len() != 1 { - return Err(Error::Schema { - message: "Multi-column (composite) indices are not yet supported".to_string(), - }); - } - self.dataset.ensure_mutable()?; - let mut dataset = (*self.dataset.get().await?).clone(); - let (column, field) = Self::resolve_index_field(dataset.schema(), &opts.columns[0])?; + let prepared = self.prepare_index(&opts).await?; + self.build_index(opts, prepared).await + } - let lance_idx_params = self.make_index_params(&field, opts.index.clone()).await?; - let index_type = self.get_index_type_for_field(&field, &opts.index); - let columns = [column.as_str()]; - let mut builder = dataset - .create_index_builder(&columns, index_type, lance_idx_params.as_ref()) - .train(opts.train) - .replace(opts.replace); - - if let Some(name) = opts.name { - builder = builder.name(name); - } - builder.await?; - self.dataset.update(dataset); - Ok(()) + async fn create_index_async(&self, opts: IndexBuilder) -> Result { + // Prepare before spawning so bad input is reported by this call rather + // than only by the job. + let prepared = self.prepare_index(&opts).await?; + let table = self.clone(); + Ok(Job::spawned(tokio::spawn(async move { + table.build_index(opts, prepared).await + }))) } async fn drop_index(&self, index_name: &str) -> Result<()> { diff --git a/rust/lancedb/src/table/create_index.rs b/rust/lancedb/src/table/create_index.rs index ab412a502..144c6dbfb 100644 --- a/rust/lancedb/src/table/create_index.rs +++ b/rust/lancedb/src/table/create_index.rs @@ -21,6 +21,9 @@ use lance_index::vector::pq::PQBuildParams; use lance_index::vector::sq::builder::SQBuildParams; use crate::error::{Error, Result}; + +/// Resolved column, index parameters and index type for one build. +pub(super) type PreparedIndex = (String, Box, IndexType); use crate::index::Index; use crate::index::vector::{VectorIndex, suggested_num_sub_vectors}; use crate::utils::{ @@ -105,6 +108,47 @@ impl NativeTable { } } + /// Resolves the target column and index parameters, erroring on input the + /// build would reject. + pub(super) async fn prepare_index( + &self, + opts: &crate::index::IndexBuilder, + ) -> Result { + if opts.columns.len() != 1 { + return Err(Error::Schema { + message: "Multi-column (composite) indices are not yet supported".to_string(), + }); + } + self.dataset.ensure_mutable()?; + let dataset = self.dataset.get().await?; + let (column, field) = Self::resolve_index_field(dataset.schema(), &opts.columns[0])?; + let params = self.make_index_params(&field, opts.index.clone()).await?; + let index_type = self.get_index_type_for_field(&field, &opts.index); + Ok((column, params, index_type)) + } + + /// Builds a prepared index and publishes the new dataset version. + pub(super) async fn build_index( + &self, + opts: crate::index::IndexBuilder, + prepared: PreparedIndex, + ) -> Result<()> { + let (column, lance_idx_params, index_type) = prepared; + let mut dataset = (*self.dataset.get().await?).clone(); + let columns = [column.as_str()]; + let mut builder = dataset + .create_index_builder(&columns, index_type, lance_idx_params.as_ref()) + .train(opts.train) + .replace(opts.replace); + + if let Some(name) = opts.name { + builder = builder.name(name); + } + builder.await?; + self.dataset.update(dataset); + Ok(()) + } + pub(super) fn resolve_index_field( schema: &lance_core::datatypes::Schema, column: &str, @@ -475,6 +519,208 @@ mod tests { assert_eq!(table.list_indices().await.unwrap().len(), 0); } + #[tokio::test] + async fn test_execute_async_job_waits_for_local_build() { + let tmp_dir = tempdir().unwrap(); + let conn = connect(tmp_dir.path().to_str().unwrap()) + .execute() + .await + .unwrap(); + let batch = record_batch!(("id", Int32, (0..512).collect::>())).unwrap(); + let table = conn.create_table("t", batch).execute().await.unwrap(); + + let job = table + .create_index(&["id"], Index::BTree(BTreeIndexBuilder::default())) + .execute_async() + .await + .unwrap(); + // Local jobs run in this process and have no server id. + assert_eq!(job.id(), None); + // The build runs as a task, so the index need not exist yet; it must + // once the job resolves. + job.wait().await.unwrap(); + assert_eq!(table.list_indices().await.unwrap().len(), 1); + // Cancelling a finished job is a no-op. + job.cancel().await.unwrap(); + } + + /// Concurrent waiters, and a wait issued after the job settled, all + /// succeed once the build does. + #[tokio::test] + async fn test_execute_async_job_reports_success_to_every_waiter() { + let tmp_dir = tempdir().unwrap(); + let conn = connect(tmp_dir.path().to_str().unwrap()) + .execute() + .await + .unwrap(); + let batch = record_batch!(("id", Int32, (0..512).collect::>())).unwrap(); + let table = conn.create_table("t", batch).execute().await.unwrap(); + + let job = Arc::new( + table + .create_index(&["id"], Index::BTree(BTreeIndexBuilder::default())) + .execute_async() + .await + .unwrap(), + ); + let waiters = (0..4) + .map(|_| { + let job = job.clone(); + tokio::spawn(async move { job.wait().await }) + }) + .collect::>(); + for waiter in waiters { + waiter.await.unwrap().unwrap(); + } + // A wait after the job settled still reports the same outcome. + job.wait().await.unwrap(); + assert_eq!(table.list_indices().await.unwrap().len(), 1); + } + + /// Every waiter sees a failure, not just the first: a waiter that missed + /// the outcome would be told the job succeeded. + #[tokio::test] + async fn test_execute_async_job_reports_failure_to_every_waiter() { + let tmp_dir = tempdir().unwrap(); + let conn = connect(tmp_dir.path().to_str().unwrap()) + .execute() + .await + .unwrap(); + let batch = record_batch!(("id", Int32, (0..512).collect::>())).unwrap(); + let table = conn.create_table("t", batch).execute().await.unwrap(); + table + .create_index(&["id"], Index::BTree(BTreeIndexBuilder::default())) + .execute() + .await + .unwrap(); + + // Rebuilding the same index without replace fails once the build + // starts, so the failure reaches the job rather than execute_async. + let job = Arc::new( + table + .create_index(&["id"], Index::BTree(BTreeIndexBuilder::default())) + .replace(false) + .execute_async() + .await + .unwrap(), + ); + let waiters = (0..3) + .map(|_| { + let job = job.clone(); + tokio::spawn(async move { job.wait().await }) + }) + .collect::>(); + for waiter in waiters { + waiter + .await + .unwrap() + .expect_err("every waiter must see the failure"); + } + job.wait().await.expect_err("a later wait still fails"); + } + + /// A local failure keeps the error it failed with, so a caller can match on + /// the original variant rather than parse a message. + #[tokio::test] + async fn test_execute_async_failure_keeps_the_source_error() { + let tmp_dir = tempdir().unwrap(); + let conn = connect(tmp_dir.path().to_str().unwrap()) + .execute() + .await + .unwrap(); + let batch = record_batch!(("id", Int32, (0..512).collect::>())).unwrap(); + let table = conn.create_table("t", batch).execute().await.unwrap(); + table + .create_index(&["id"], Index::BTree(BTreeIndexBuilder::default())) + .execute() + .await + .unwrap(); + + let job = table + .create_index(&["id"], Index::BTree(BTreeIndexBuilder::default())) + .replace(false) + .execute_async() + .await + .unwrap(); + + let crate::Error::JobFailed { failure, .. } = job.wait().await.unwrap_err() else { + panic!("a failed job reports JobFailed"); + }; + let source = failure.source.expect("a local failure carries its error"); + assert_eq!( + failure.message.as_deref(), + Some(source.to_string()).as_deref() + ); + // Nothing local can report these, so they must be absent rather than invented. + assert!(failure.phase.is_none()); + assert!(failure.retryable.is_none()); + } + + /// Every waiter sees the cancellation, including ones that were already + /// waiting when the cancel landed. + #[tokio::test] + async fn test_execute_async_job_reports_cancellation_to_every_waiter() { + let tmp_dir = tempdir().unwrap(); + let conn = connect(tmp_dir.path().to_str().unwrap()) + .execute() + .await + .unwrap(); + let batch = record_batch!(("id", Int32, (0..512).collect::>())).unwrap(); + let table = conn.create_table("t", batch).execute().await.unwrap(); + + let job = Arc::new( + table + .create_index(&["id"], Index::BTree(BTreeIndexBuilder::default())) + .execute_async() + .await + .unwrap(), + ); + // Cancel before yielding, so the build cannot have started and the + // outcome is always the cancellation. + job.cancel().await.unwrap(); + + let waiters = (0..2) + .map(|_| { + let job = job.clone(); + tokio::spawn(async move { job.wait().await }) + }) + .collect::>(); + for waiter in waiters { + match waiter.await.unwrap() { + Err(crate::Error::JobCancelled { .. }) => {} + other => panic!("expected the cancellation, got {other:?}"), + } + } + match job.wait().await { + Err(crate::Error::JobCancelled { .. }) => {} + other => panic!("expected the cancellation, got {other:?}"), + } + } + + #[tokio::test] + async fn test_execute_async_job_cancel_stops_local_build() { + let tmp_dir = tempdir().unwrap(); + let conn = connect(tmp_dir.path().to_str().unwrap()) + .execute() + .await + .unwrap(); + let batch = record_batch!(("id", Int32, (0..512).collect::>())).unwrap(); + let table = conn.create_table("t", batch).execute().await.unwrap(); + + let job = table + .create_index(&["id"], Index::BTree(BTreeIndexBuilder::default())) + .execute_async() + .await + .unwrap(); + job.cancel().await.unwrap(); + match job.wait().await { + Err(crate::Error::JobCancelled { .. }) => {} + // The build may finish before the abort lands. + Ok(()) => {} + other => panic!("unexpected job outcome: {other:?}"), + } + } + #[tokio::test] async fn test_ivf_pq_uses_default_partition_size_for_num_partitions() { use crate::index::vector::IvfPqIndexBuilder; From e3b472c212c215e8783d6b43d544c68658166d3e Mon Sep 17 00:00:00 2001 From: Wyatt Alt Date: Fri, 31 Jul 2026 12:51:43 -0700 Subject: [PATCH 24/29] feat: connection-level job operations (#3755) Adds job operations to the connection surface, building on the Job handle from #3742: job(id), list_jobs, get_job, cancel_job, and job_history, plus a non-blocking Job.status(). Implemented on the Database trait (defaulting to NotSupported), the remote backend (/v1/jobs), and the Python and Node bindings; job_history returns Arrow batches. errors() and progress() are not included. Tested with mocked endpoints in all three languages. --------- Co-authored-by: Claude Fable 5 --- docs/src/js/classes/Connection.md | 97 +++++++ docs/src/js/classes/Job.md | 19 ++ docs/src/js/globals.md | 3 + docs/src/js/interfaces/JobDescription.md | 66 +++++ docs/src/js/interfaces/JobFailureInfo.md | 33 +++ docs/src/js/interfaces/JobInfo.md | 58 ++++ nodejs/__test__/remote.test.ts | 93 +++++++ nodejs/lancedb/connection.ts | 62 +++++ nodejs/lancedb/index.ts | 8 +- nodejs/src/connection.rs | 63 +++++ nodejs/src/job.rs | 79 ++++++ python/python/lancedb/_lancedb.pyi | 42 +++ python/python/lancedb/db.py | 120 ++++++++ python/python/lancedb/job.py | 22 ++ python/python/lancedb/remote/db.py | 47 +++- python/python/tests/test_remote_db.py | 116 ++++++++ python/src/connection.rs | 57 +++- python/src/job.rs | 99 +++++++ python/src/lib.rs | 3 + rust/lancedb/src/connection.rs | 37 ++- rust/lancedb/src/database.rs | 66 +++++ rust/lancedb/src/job.rs | 24 ++ rust/lancedb/src/remote/db.rs | 332 ++++++++++++++++++++++- rust/lancedb/src/remote/job.rs | 37 ++- 24 files changed, 1569 insertions(+), 14 deletions(-) create mode 100644 docs/src/js/interfaces/JobDescription.md create mode 100644 docs/src/js/interfaces/JobFailureInfo.md create mode 100644 docs/src/js/interfaces/JobInfo.md diff --git a/docs/src/js/classes/Connection.md b/docs/src/js/classes/Connection.md index ad24644b6..fa4e0748a 100644 --- a/docs/src/js/classes/Connection.md +++ b/docs/src/js/classes/Connection.md @@ -25,6 +25,27 @@ the underlying connection has been closed. ## Methods +### cancelJob() + +```ts +abstract cancelJob(jobId): Promise +``` + +Request cancellation of a server-side job by id. + +Resolves to true if the server accepted the cancellation, false if no +such job exists. Cancelling an already-terminal job is a no-op success. + +#### Parameters + +* **jobId**: `string` + +#### Returns + +`Promise`<`boolean`> + +*** + ### cloneTable() ```ts @@ -365,6 +386,26 @@ Drop an existing table. *** +### getJob() + +```ts +abstract getJob(jobId): Promise +``` + +Describe a single server-side job by id. + +Resolves to `null` when the server has no such job. + +#### Parameters + +* **jobId**: `string` + +#### Returns + +`Promise`<`null` \| [`JobDescription`](../interfaces/JobDescription.md)> + +*** + ### isOpen() ```ts @@ -379,6 +420,62 @@ Return true if the connection has not been closed *** +### job() + +```ts +abstract job(jobId): Job +``` + +A [Job](Job.md) handle for a server-side job by id. + +The handle is constructed without a server round trip; an unknown id +surfaces when the handle is used. Dropping the handle has no effect on +the job itself. + +#### Parameters + +* **jobId**: `string` + +#### Returns + +[`Job`](Job.md) + +*** + +### jobHistory() + +```ts +abstract jobHistory(jobId?): Promise> +``` + +The lifecycle event history of a server-side job, as an Arrow table. + +Lists history across all jobs when `jobId` is omitted. + +#### Parameters + +* **jobId?**: `string` + +#### Returns + +`Promise`<`Table`<`any`>> + +*** + +### listJobs() + +```ts +abstract listJobs(): Promise +``` + +List server-side jobs across the database's tables. + +#### Returns + +`Promise`<[`JobInfo`](../interfaces/JobInfo.md)[]> + +*** + ### listNamespaces() ```ts diff --git a/docs/src/js/classes/Job.md b/docs/src/js/classes/Job.md index c46b9651f..9f723e9e8 100644 --- a/docs/src/js/classes/Job.md +++ b/docs/src/js/classes/Job.md @@ -51,6 +51,25 @@ Request cancellation. Cancelling a finished operation is a no-op. *** +### status() + +```ts +status(): Promise +``` + +The operation's current lifecycle state: "running", "finished", +"failed", or "cancelled". + +A point snapshot; unlike [Job.wait](Job.md#wait) it does not block or reject +on a terminal failure state. States a newer server reports that this +client version does not know pass through as-is. + +#### Returns + +`Promise`<`string`> + +*** + ### wait() ```ts diff --git a/docs/src/js/globals.md b/docs/src/js/globals.md index 323a24a49..7455a81ce 100644 --- a/docs/src/js/globals.md +++ b/docs/src/js/globals.md @@ -89,6 +89,9 @@ - [IvfFlatOptions](interfaces/IvfFlatOptions.md) - [IvfPqOptions](interfaces/IvfPqOptions.md) - [IvfRqOptions](interfaces/IvfRqOptions.md) +- [JobDescription](interfaces/JobDescription.md) +- [JobFailureInfo](interfaces/JobFailureInfo.md) +- [JobInfo](interfaces/JobInfo.md) - [ListNamespacesOptions](interfaces/ListNamespacesOptions.md) - [ListNamespacesResponse](interfaces/ListNamespacesResponse.md) - [LsmWriteSpec](interfaces/LsmWriteSpec.md) diff --git a/docs/src/js/interfaces/JobDescription.md b/docs/src/js/interfaces/JobDescription.md new file mode 100644 index 000000000..5118bf5d6 --- /dev/null +++ b/docs/src/js/interfaces/JobDescription.md @@ -0,0 +1,66 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / JobDescription + +# Interface: JobDescription + +A described job from `Connection.getJob`. + +## Properties + +### creationMs + +```ts +creationMs: number; +``` + +When the job was created, in milliseconds since the epoch. + +*** + +### failure? + +```ts +optional failure: JobFailureInfo; +``` + +Why the job failed, when the job is failed and the server reports a +reason. + +*** + +### jobId + +```ts +jobId: string; +``` + +*** + +### jobType + +```ts +jobType: string; +``` + +*** + +### specJson? + +```ts +optional specJson: string; +``` + +The job-type-specific specification as a JSON string, when present. + +*** + +### state + +```ts +state: string; +``` + +Lifecycle state: "running", "finished", "failed", or "cancelled". diff --git a/docs/src/js/interfaces/JobFailureInfo.md b/docs/src/js/interfaces/JobFailureInfo.md new file mode 100644 index 000000000..5683ee5f0 --- /dev/null +++ b/docs/src/js/interfaces/JobFailureInfo.md @@ -0,0 +1,33 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / JobFailureInfo + +# Interface: JobFailureInfo + +The server's account of why a job failed. + +## Properties + +### message? + +```ts +optional message: string; +``` + +*** + +### phase? + +```ts +optional phase: string; +``` + +*** + +### retryable? + +```ts +optional retryable: boolean; +``` diff --git a/docs/src/js/interfaces/JobInfo.md b/docs/src/js/interfaces/JobInfo.md new file mode 100644 index 000000000..3596fc968 --- /dev/null +++ b/docs/src/js/interfaces/JobInfo.md @@ -0,0 +1,58 @@ +[**@lancedb/lancedb**](../README.md) • **Docs** + +*** + +[@lancedb/lancedb](../globals.md) / JobInfo + +# Interface: JobInfo + +A row from `Connection.listJobs`: one server-side job. + +## Properties + +### createdAtMillis + +```ts +createdAtMillis: number; +``` + +When the job was created, in milliseconds since the epoch. + +*** + +### jobId + +```ts +jobId: string; +``` + +The job id -- what `Connection.getJob` and `Connection.cancelJob` +accept. + +*** + +### jobType + +```ts +jobType: string; +``` + +*** + +### state + +```ts +state: string; +``` + +Lifecycle state: "running", "finished", "failed", or "cancelled". + +*** + +### table + +```ts +table: string; +``` + +The table the job runs against, without URI or namespace. diff --git a/nodejs/__test__/remote.test.ts b/nodejs/__test__/remote.test.ts index 75c5540eb..89a9e992c 100644 --- a/nodejs/__test__/remote.test.ts +++ b/nodejs/__test__/remote.test.ts @@ -877,3 +877,96 @@ describe("remote connection", () => { }); }); }); + +describe("remote connection jobs surface", () => { + it("lists, describes, cancels, and reads history", async () => { + const { tableFromArrays, tableToIPC } = await import("apache-arrow"); + const eventsTable = tableFromArrays({ state: ["created", "succeeded"] }); + const eventsBody = Buffer.from(tableToIPC(eventsTable, "stream")); + + await withMockDatabase( + (req, res) => { + let body = ""; + req.on("data", (chunk) => { + body += chunk; + }); + req.on("end", () => { + const payload = body.length > 0 ? JSON.parse(body) : {}; + if (req.url === "/v1/jobs/list") { + if (payload["page_token"] === undefined) { + res + .writeHead(200, { "Content-Type": "application/json" }) + .end( + '{"jobs": [{"job_id": "job-1", "table": "t1", ' + + '"job_type": "create_index", "state": "in_progress", ' + + '"created_at_millis": 1000}], "page_token": "next"}', + ); + } else { + res + .writeHead(200, { "Content-Type": "application/json" }) + .end( + '{"jobs": [{"job_id": "job-2", "table": "t2", ' + + '"job_type": "create_index", "state": "succeeded", ' + + '"created_at_millis": 2000}]}', + ); + } + } else if (req.url === "/v1/jobs/describe") { + if (payload["job_id"] !== "job-1") { + res.writeHead(404).end("no such job"); + return; + } + res + .writeHead(200, { "Content-Type": "application/json" }) + .end( + '{"job_id": "job-1", "job_type": "create_index", ' + + '"job_state": "FAILED", "creation_ms": 1000, ' + + '"spec": {"column": "vec"}, "failure": {"phase": "execute", ' + + '"message": "worker died", "retryable": true}}', + ); + } else if (req.url === "/v1/jobs/cancel") { + if (payload["job_id"] !== "job-1") { + res.writeHead(404).end("no such job"); + return; + } + res + .writeHead(200, { "Content-Type": "application/json" }) + .end('{"job_id": "job-1"}'); + } else if (req.url === "/v1/jobs/query_events") { + res + .writeHead(200, { + "Content-Type": "application/vnd.apache.arrow.stream", + }) + .end(eventsBody); + } else { + res.writeHead(404).end(); + } + }); + }, + async (db) => { + const jobs = await db.listJobs(); + expect(jobs.map((job) => job.jobId)).toEqual(["job-1", "job-2"]); + expect(jobs[0].state).toEqual("running"); + expect(jobs[1].state).toEqual("finished"); + + const description = await db.getJob("job-1"); + expect(description?.state).toEqual("failed"); + expect(JSON.parse(description?.specJson ?? "")).toEqual({ + column: "vec", + }); + expect(description?.failure?.message).toEqual("worker died"); + expect(await db.getJob("missing")).toBeNull(); + + expect(await db.cancelJob("job-1")).toBe(true); + expect(await db.cancelJob("missing")).toBe(false); + + const history = await db.jobHistory("job-1"); + expect(history.numRows).toEqual(2); + + const job = db.job("job-1"); + expect(job.id).toEqual("job-1"); + expect(await job.status()).toEqual("failed"); + await expect(job.wait()).rejects.toThrow("worker died"); + }, + ); + }); +}); diff --git a/nodejs/lancedb/connection.ts b/nodejs/lancedb/connection.ts index ef03e276b..e63a7ae65 100644 --- a/nodejs/lancedb/connection.ts +++ b/nodejs/lancedb/connection.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright The LanceDB Authors +import { tableFromIPC } from "apache-arrow"; import { Data, SchemaLike, @@ -20,6 +21,9 @@ import type { CreateNamespaceResponse, DescribeNamespaceResponse, DropNamespaceResponse, + Job, + JobDescription, + JobInfo, ListNamespacesResponse, } from "./native"; export type { @@ -436,6 +440,40 @@ export abstract class Connection { newName: string, options?: RenameTableOptions, ): Promise; + + /** + * A {@link Job} handle for a server-side job by id. + * + * The handle is constructed without a server round trip; an unknown id + * surfaces when the handle is used. Dropping the handle has no effect on + * the job itself. + */ + abstract job(jobId: string): Job; + + /** List server-side jobs across the database's tables. */ + abstract listJobs(): Promise; + + /** + * Describe a single server-side job by id. + * + * Resolves to `null` when the server has no such job. + */ + abstract getJob(jobId: string): Promise; + + /** + * Request cancellation of a server-side job by id. + * + * Resolves to true if the server accepted the cancellation, false if no + * such job exists. Cancelling an already-terminal job is a no-op success. + */ + abstract cancelJob(jobId: string): Promise; + + /** + * The lifecycle event history of a server-side job, as an Arrow table. + * + * Lists history across all jobs when `jobId` is omitted. + */ + abstract jobHistory(jobId?: string): Promise; } /** @hideconstructor */ @@ -722,6 +760,30 @@ export class LocalConnection extends Connection { options?.newNamespacePath, ); } + + job(jobId: string): Job { + return this.inner.job(jobId); + } + + async listJobs(): Promise { + return this.inner.listJobs(); + } + + async getJob(jobId: string): Promise { + return this.inner.getJob(jobId); + } + + async cancelJob(jobId: string): Promise { + return this.inner.cancelJob(jobId); + } + + async jobHistory(jobId?: string): Promise { + const buf = await this.inner.jobHistory(jobId); + if (buf.length === 0) { + return new ArrowTable(); + } + return tableFromIPC(buf); + } } /** diff --git a/nodejs/lancedb/index.ts b/nodejs/lancedb/index.ts index 2ce031458..319222421 100644 --- a/nodejs/lancedb/index.ts +++ b/nodejs/lancedb/index.ts @@ -85,7 +85,13 @@ export { RenameTableOptions, } from "./connection"; -export { Job, Session } from "./native.js"; +export { + Job, + JobDescription, + JobFailureInfo, + JobInfo, + Session, +} from "./native.js"; export { ExecutableQuery, diff --git a/nodejs/src/connection.rs b/nodejs/src/connection.rs index 1a18651f0..c45321aba 100644 --- a/nodejs/src/connection.rs +++ b/nodejs/src/connection.rs @@ -340,6 +340,69 @@ impl Connection { self.get_inner()?.drop_all_tables(&ns).await.default_error() } + /// A `Job` handle for a server-side job by id. + /// + /// The handle is constructed without a server round trip; an unknown id + /// surfaces when the handle is used. + #[napi] + pub fn job(&self, job_id: String) -> napi::Result { + let job = self.get_inner()?.job(job_id).default_error()?; + Ok(crate::job::Job::new(job)) + } + + /// List server-side jobs across the database's tables. + #[napi(catch_unwind)] + pub async fn list_jobs(&self) -> napi::Result> { + let jobs = self.get_inner()?.list_jobs().await.default_error()?; + Ok(jobs.into_iter().map(Into::into).collect()) + } + + /// Describe a single server-side job by id. `null` when the server has + /// no such job. + #[napi(catch_unwind)] + pub async fn get_job( + &self, + job_id: String, + ) -> napi::Result> { + let description = self.get_inner()?.get_job(&job_id).await.default_error()?; + Ok(description.map(Into::into)) + } + + /// Request cancellation of a server-side job by id. Returns true if the + /// server accepted the cancellation, false if no such job exists. + #[napi(catch_unwind)] + pub async fn cancel_job(&self, job_id: String) -> napi::Result { + self.get_inner()?.cancel_job(&job_id).await.default_error() + } + + /// The lifecycle event history of a server-side job (all jobs when + /// `job_id` is null), as an Arrow IPC stream buffer. Empty when there is + /// no history. + #[napi(catch_unwind)] + pub async fn job_history(&self, job_id: Option) -> napi::Result { + let batches = self + .get_inner()? + .job_history(job_id.as_deref()) + .await + .default_error()?; + let Some(first) = batches.first() else { + return Ok(Buffer::from(Vec::::new())); + }; + let mut out = Vec::new(); + let mut writer = arrow_ipc::writer::StreamWriter::try_new(&mut out, &first.schema()) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + for batch in &batches { + writer + .write(batch) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + } + writer + .finish() + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + drop(writer); + Ok(Buffer::from(out)) + } + #[napi(catch_unwind)] /// Describe a namespace and return its properties. pub async fn describe_namespace( diff --git a/nodejs/src/job.rs b/nodejs/src/job.rs index 52d082beb..6aaeee174 100644 --- a/nodejs/src/job.rs +++ b/nodejs/src/job.rs @@ -30,6 +30,17 @@ impl Job { self.inner.id().map(str::to_string) } + /// The operation's current lifecycle state: "running", "finished", + /// "failed", or "cancelled". + /// + /// A point snapshot; unlike {@link Job.wait} it does not block or reject + /// on a terminal failure state. States a newer server reports that this + /// client version does not know pass through as-is. + #[napi(catch_unwind)] + pub async fn status(&self) -> napi::Result { + self.inner.status().await.default_error() + } + /// Wait until the operation reaches a terminal state. #[napi(catch_unwind)] pub async fn wait(&self) -> napi::Result<()> { @@ -42,3 +53,71 @@ impl Job { self.inner.cancel().await.default_error() } } + +/// A row from `Connection.listJobs`: one server-side job. +#[napi(object)] +pub struct JobInfo { + /// The job id -- what `Connection.getJob` and `Connection.cancelJob` + /// accept. + pub job_id: String, + /// The table the job runs against, without URI or namespace. + pub table: String, + pub job_type: String, + /// Lifecycle state: "running", "finished", "failed", or "cancelled". + pub state: String, + /// When the job was created, in milliseconds since the epoch. + pub created_at_millis: i64, +} + +impl From for JobInfo { + fn from(info: lancedb::database::JobInfo) -> Self { + Self { + job_id: info.job_id, + table: info.table, + job_type: info.job_type, + state: info.state, + created_at_millis: info.created_at_millis, + } + } +} + +/// The server's account of why a job failed. +#[napi(object)] +pub struct JobFailureInfo { + pub phase: Option, + pub message: Option, + pub retryable: Option, +} + +/// A described job from `Connection.getJob`. +#[napi(object)] +pub struct JobDescription { + pub job_id: String, + pub job_type: String, + /// Lifecycle state: "running", "finished", "failed", or "cancelled". + pub state: String, + /// When the job was created, in milliseconds since the epoch. + pub creation_ms: i64, + /// The job-type-specific specification as a JSON string, when present. + pub spec_json: Option, + /// Why the job failed, when the job is failed and the server reports a + /// reason. + pub failure: Option, +} + +impl From for JobDescription { + fn from(description: lancedb::database::JobDescription) -> Self { + Self { + job_id: description.job_id, + job_type: description.job_type, + state: description.state, + creation_ms: description.creation_ms, + spec_json: (!description.spec.is_null()).then(|| description.spec.to_string()), + failure: description.failure.map(|failure| JobFailureInfo { + phase: failure.phase, + message: failure.message, + retryable: failure.retryable, + }), + } + } +} diff --git a/python/python/lancedb/_lancedb.pyi b/python/python/lancedb/_lancedb.pyi index f5c59c152..47e727f99 100644 --- a/python/python/lancedb/_lancedb.pyi +++ b/python/python/lancedb/_lancedb.pyi @@ -146,6 +146,13 @@ class Connection(object): start_after: Optional[str], limit: Optional[int], ) -> list[str]: ... # Deprecated: Use list_tables instead + def job(self, job_id: str) -> Job: ... + async def list_jobs(self) -> List[JobInfo]: ... + async def get_job(self, job_id: str) -> Optional[JobDescription]: ... + async def cancel_job(self, job_id: str) -> bool: ... + async def job_history( + self, job_id: Optional[str] = None + ) -> List[pa.RecordBatch]: ... async def create_table( self, name: str, @@ -212,9 +219,44 @@ class BlobFile: class Job: @property def id(self) -> Optional[str]: ... + async def status(self) -> str: ... async def wait(self) -> None: ... async def cancel(self) -> None: ... +class JobInfo: + @property + def job_id(self) -> str: ... + @property + def table(self) -> str: ... + @property + def job_type(self) -> str: ... + @property + def state(self) -> str: ... + @property + def created_at_millis(self) -> int: ... + +class JobFailureInfo: + @property + def phase(self) -> Optional[str]: ... + @property + def message(self) -> Optional[str]: ... + @property + def retryable(self) -> Optional[bool]: ... + +class JobDescription: + @property + def job_id(self) -> str: ... + @property + def job_type(self) -> str: ... + @property + def state(self) -> str: ... + @property + def creation_ms(self) -> int: ... + @property + def spec_json(self) -> Optional[str]: ... + @property + def failure(self) -> Optional[JobFailureInfo]: ... + class Table: def name(self) -> str: ... def __repr__(self) -> str: ... diff --git a/python/python/lancedb/db.py b/python/python/lancedb/db.py index 90b8dc75b..b48e84cc5 100644 --- a/python/python/lancedb/db.py +++ b/python/python/lancedb/db.py @@ -45,6 +45,7 @@ from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError from . import __version__ from ._lancedb import connect as lancedb_connect # type: ignore +from .job import AsyncJob, Job from .table import ( AsyncTable, LanceTable, @@ -63,6 +64,7 @@ if TYPE_CHECKING: from .pydantic import LanceModel from ._lancedb import Connection as LanceDbConnection + from ._lancedb import JobDescription, JobInfo from .common import DATA, URI from .embeddings import EmbeddingFunctionConfig from ._lancedb import Session @@ -608,6 +610,46 @@ class DBConnection(EnforceOverrides): """ raise NotImplementedError("serialize is not supported for this connection type") + def job(self, job_id: str) -> Job: + """A [Job][lancedb.job.Job] handle for a server-side job by id. + + The handle is constructed without a server round trip; an unknown id + surfaces when the handle is used. Dropping the handle has no effect + on the job itself. + """ + raise NotImplementedError("job is not supported for this connection type") + + def list_jobs(self) -> List[JobInfo]: + """List server-side jobs across the database's tables.""" + raise NotImplementedError("list_jobs is not supported for this connection type") + + def get_job(self, job_id: str) -> Optional[JobDescription]: + """Describe a single server-side job by id. + + Returns None when the server has no such job. + """ + raise NotImplementedError("get_job is not supported for this connection type") + + def cancel_job(self, job_id: str) -> bool: + """Request cancellation of a server-side job by id. + + Returns True if the server accepted the cancellation, False if no + such job exists. Cancelling an already-terminal job is a no-op + success. + """ + raise NotImplementedError( + "cancel_job is not supported for this connection type" + ) + + def job_history(self, job_id: Optional[str] = None) -> List[pa.RecordBatch]: + """The lifecycle event history of a server-side job, as Arrow batches. + + Lists history across all jobs when `job_id` is None. + """ + raise NotImplementedError( + "job_history is not supported for this connection type" + ) + class LanceDBConnection(DBConnection): """ @@ -1170,6 +1212,47 @@ class LanceDBConnection(DBConnection): ) ) + @override + def job(self, job_id: str) -> Job: + """A [Job][lancedb.job.Job] handle for a server-side job by id. + + The handle is constructed without a server round trip; an unknown id + surfaces when the handle is used. Dropping the handle has no effect + on the job itself. + """ + return Job(self._conn.job(job_id)) + + @override + def list_jobs(self) -> List[JobInfo]: + """List server-side jobs across the database's tables.""" + return LOOP.run(self._conn.list_jobs()) + + @override + def get_job(self, job_id: str) -> Optional[JobDescription]: + """Describe a single server-side job by id. + + Returns None when the server has no such job. + """ + return LOOP.run(self._conn.get_job(job_id)) + + @override + def cancel_job(self, job_id: str) -> bool: + """Request cancellation of a server-side job by id. + + Returns True if the server accepted the cancellation, False if no + such job exists. Cancelling an already-terminal job is a no-op + success. + """ + return LOOP.run(self._conn.cancel_job(job_id)) + + @override + def job_history(self, job_id: Optional[str] = None) -> List[pa.RecordBatch]: + """The lifecycle event history of a server-side job, as Arrow batches. + + Lists history across all jobs when `job_id` is None. + """ + return LOOP.run(self._conn.job_history(job_id)) + @override def namespace_client(self) -> LanceNamespace: """Get the equivalent namespace client for this connection. @@ -1879,6 +1962,43 @@ class AsyncConnection(object): namespace_path = [] await self._inner.drop_all_tables(namespace_path=namespace_path) + def job(self, job_id: str) -> AsyncJob: + """An [AsyncJob][lancedb.job.AsyncJob] handle for a server-side job + by id. + + The handle is constructed without a server round trip; an unknown id + surfaces when the handle is used. Dropping the handle has no effect + on the job itself. + """ + return AsyncJob(self._inner.job(job_id)) + + async def list_jobs(self) -> List[JobInfo]: + """List server-side jobs across the database's tables.""" + return await self._inner.list_jobs() + + async def get_job(self, job_id: str) -> Optional[JobDescription]: + """Describe a single server-side job by id. + + Returns None when the server has no such job. + """ + return await self._inner.get_job(job_id) + + async def cancel_job(self, job_id: str) -> bool: + """Request cancellation of a server-side job by id. + + Returns True if the server accepted the cancellation, False if no + such job exists. Cancelling an already-terminal job is a no-op + success. + """ + return await self._inner.cancel_job(job_id) + + async def job_history(self, job_id: Optional[str] = None) -> List[pa.RecordBatch]: + """The lifecycle event history of a server-side job, as Arrow batches. + + Lists history across all jobs when `job_id` is None. + """ + return await self._inner.job_history(job_id) + async def namespace_client(self) -> LanceNamespace: """Get the equivalent namespace client for this connection. diff --git a/python/python/lancedb/job.py b/python/python/lancedb/job.py index 82911119b..d33b62cbf 100644 --- a/python/python/lancedb/job.py +++ b/python/python/lancedb/job.py @@ -32,6 +32,18 @@ class AsyncJob: """ return self._inner.id if self._inner is not None else None + async def status(self) -> str: + """The operation's current lifecycle state: "running", "finished", + "failed", or "cancelled". + + A point snapshot; unlike `wait` it does not block or raise on a + terminal failure state. States a newer server reports that this + client version does not know pass through as-is. + """ + if self._inner is None: + return "finished" + return await self._inner.status() + async def wait(self, timeout: Optional[timedelta] = None): """Wait until the operation reaches a terminal state. @@ -66,6 +78,16 @@ class Job: """ return self._inner.id if self._inner is not None else None + def status(self) -> str: + """The operation's current lifecycle state: "running", "finished", + "failed", or "cancelled". + + See :meth:`AsyncJob.status`. + """ + if self._inner is None: + return "finished" + return LOOP.run(self._inner.status()) + def wait(self, timeout: Optional[timedelta] = None): """Block until the operation reaches a terminal state. diff --git a/python/python/lancedb/remote/db.py b/python/python/lancedb/remote/db.py index 3171c3638..332886590 100644 --- a/python/python/lancedb/remote/db.py +++ b/python/python/lancedb/remote/db.py @@ -7,7 +7,7 @@ import json import logging from concurrent.futures import ThreadPoolExecutor import sys -from typing import Any, Dict, Iterable, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union from urllib.parse import urlparse import warnings @@ -23,6 +23,10 @@ import pyarrow as pa from ..common import DATA from ..db import DBConnection, LOOP +from ..job import Job + +if TYPE_CHECKING: + from .._lancedb import JobDescription, JobInfo from ..embeddings import EmbeddingFunctionConfig from lance_namespace import ( LanceNamespace, @@ -689,6 +693,47 @@ class RemoteDBConnection(DBConnection): ) ) + @override + def job(self, job_id: str) -> Job: + """A [Job][lancedb.job.Job] handle for a server-side job by id. + + The handle is constructed without a server round trip; an unknown id + surfaces when the handle is used. Dropping the handle has no effect + on the job itself. + """ + return Job(self._conn.job(job_id)) + + @override + def list_jobs(self) -> List["JobInfo"]: + """List server-side jobs across the database's tables.""" + return LOOP.run(self._conn.list_jobs()) + + @override + def get_job(self, job_id: str) -> Optional["JobDescription"]: + """Describe a single server-side job by id. + + Returns None when the server has no such job. + """ + return LOOP.run(self._conn.get_job(job_id)) + + @override + def cancel_job(self, job_id: str) -> bool: + """Request cancellation of a server-side job by id. + + Returns True if the server accepted the cancellation, False if no + such job exists. Cancelling an already-terminal job is a no-op + success. + """ + return LOOP.run(self._conn.cancel_job(job_id)) + + @override + def job_history(self, job_id: Optional[str] = None) -> List[pa.RecordBatch]: + """The lifecycle event history of a server-side job, as Arrow batches. + + Lists history across all jobs when `job_id` is None. + """ + return LOOP.run(self._conn.job_history(job_id)) + @override def namespace_client(self) -> LanceNamespace: """Get the equivalent namespace client for this connection. diff --git a/python/python/tests/test_remote_db.py b/python/python/tests/test_remote_db.py index 9bb162b15..757d8a758 100644 --- a/python/python/tests/test_remote_db.py +++ b/python/python/tests/test_remote_db.py @@ -2153,3 +2153,119 @@ def test_remote_blob_byte_apis_not_supported_on_old_server(): table.fetch_blobs("image", [1]) with pytest.raises(NotImplementedError, match="not supported"): table.fetch_blob_files("image", [1]) + + +def test_remote_connection_jobs_surface(): + from lancedb.exceptions import JobFailedError + + schema = pa.schema([("state", pa.string())]) + batch = pa.record_batch([pa.array(["created", "done"])], schema=schema) + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, schema) as writer: + writer.write_batch(batch) + events_body = sink.getvalue().to_pybytes() + + def handler(request): + content_len = int(request.headers.get("Content-Length", 0)) + body = request.rfile.read(content_len) if content_len > 0 else b"" + payload = json.loads(body) if body else {} + if request.path == "/v1/jobs/list": + if payload.get("page_token") is None: + rsp = dict( + jobs=[ + dict( + job_id="job-1", + table="t1", + job_type="create_index", + state="in_progress", + created_at_millis=1000, + ) + ], + page_token="next", + ) + else: + assert payload["page_token"] == "next" + rsp = dict( + jobs=[ + dict( + job_id="job-2", + table="t2", + job_type="create_index", + state="succeeded", + created_at_millis=2000, + ) + ] + ) + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write(json.dumps(rsp).encode()) + elif request.path == "/v1/jobs/describe": + if payload["job_id"] != "job-1": + request.send_response(404) + request.end_headers() + return + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write( + json.dumps( + dict( + job_id="job-1", + job_type="create_index", + job_state="FAILED", + creation_ms=1000, + spec=dict(column="vec"), + failure=dict( + phase="execute", message="worker died", retryable=True + ), + ) + ).encode() + ) + elif request.path == "/v1/jobs/cancel": + if payload["job_id"] != "job-1": + request.send_response(404) + request.end_headers() + return + request.send_response(200) + request.send_header("Content-Type", "application/json") + request.end_headers() + request.wfile.write(b'{"job_id": "job-1"}') + elif request.path == "/v1/jobs/query_events": + assert payload["job_id"] == "job-1" + request.send_response(200) + request.send_header("Content-Type", "application/vnd.apache.arrow.stream") + request.end_headers() + request.wfile.write(events_body) + else: + request.send_response(404) + request.end_headers() + + with mock_lancedb_connection(handler) as db: + jobs = db.list_jobs() + assert [job.job_id for job in jobs] == ["job-1", "job-2"] + assert jobs[0].state == "running" + assert jobs[0].table == "t1" + assert jobs[1].state == "finished" + + description = db.get_job("job-1") + assert description.job_type == "create_index" + assert description.state == "failed" + assert json.loads(description.spec_json) == {"column": "vec"} + assert description.failure.message == "worker died" + assert description.failure.retryable is True + assert db.get_job("missing") is None + + assert db.cancel_job("job-1") is True + assert db.cancel_job("missing") is False + + batches = db.job_history("job-1") + assert len(batches) == 1 + assert batches[0].num_rows == 2 + assert batches[0].column("state").to_pylist() == ["created", "done"] + + job = db.job("job-1") + assert job.id == "job-1" + assert job.status() == "failed" + with pytest.raises(JobFailedError, match="worker died"): + job.wait(timeout=timedelta(seconds=5)) diff --git a/python/src/connection.rs b/python/src/connection.rs index e22813640..b97d48ad8 100644 --- a/python/src/connection.rs +++ b/python/src/connection.rs @@ -13,7 +13,11 @@ use crate::{ runtime::future_into_py, table::Table, }; -use arrow::{datatypes::Schema, ffi_stream::ArrowArrayStreamReader, pyarrow::FromPyArrow}; +use arrow::{ + datatypes::Schema, + ffi_stream::ArrowArrayStreamReader, + pyarrow::{FromPyArrow, ToPyArrow}, +}; use lancedb::{ connection::Connection as LanceConnection, connection::NamespaceClientPushdownOperation, @@ -24,7 +28,7 @@ use pyo3::{ Bound, FromPyObject, Py, PyAny, PyRef, PyResult, Python, exceptions::{PyRuntimeError, PyValueError}, pyclass, pyfunction, pymethods, - types::{PyDict, PyDictMethods}, + types::{PyDict, PyDictMethods, PyList, PyListMethods}, }; #[pyclass] @@ -536,6 +540,55 @@ impl Connection { }) }) } + + pub fn job(&self, job_id: String) -> PyResult { + let inner = self.get_inner()?.clone(); + Ok(crate::job::Job::new(inner.job(job_id).infer_error()?)) + } + + pub fn list_jobs(self_: PyRef<'_, Self>) -> PyResult> { + let inner = self_.get_inner()?.clone(); + future_into_py(self_.py(), async move { + let jobs = inner.list_jobs().await.infer_error()?; + Ok(jobs + .into_iter() + .map(crate::job::JobInfo::from) + .collect::>()) + }) + } + + pub fn get_job(self_: PyRef<'_, Self>, job_id: String) -> PyResult> { + let inner = self_.get_inner()?.clone(); + future_into_py(self_.py(), async move { + let description = inner.get_job(&job_id).await.infer_error()?; + Ok(description.map(crate::job::JobDescription::from)) + }) + } + + pub fn cancel_job(self_: PyRef<'_, Self>, job_id: String) -> PyResult> { + let inner = self_.get_inner()?.clone(); + future_into_py(self_.py(), async move { + inner.cancel_job(&job_id).await.infer_error() + }) + } + + #[pyo3(signature = (job_id=None))] + pub fn job_history( + self_: PyRef<'_, Self>, + job_id: Option, + ) -> PyResult> { + let inner = self_.get_inner()?.clone(); + future_into_py(self_.py(), async move { + let batches = inner.job_history(job_id.as_deref()).await.infer_error()?; + Python::attach(|py| { + let list = PyList::empty(py); + for batch in batches { + list.append(batch.to_pyarrow(py)?)?; + } + Ok(list.unbind()) + }) + }) + } } #[pyfunction] diff --git a/python/src/job.rs b/python/src/job.rs index 83303ddea..56ee211f4 100644 --- a/python/src/job.rs +++ b/python/src/job.rs @@ -28,6 +28,14 @@ impl Job { self.inner.id().map(str::to_string) } + pub fn status(self_: PyRef<'_, Self>) -> PyResult> { + let inner = self_.inner.clone(); + future_into_py( + self_.py(), + async move { inner.status().await.infer_error() }, + ) + } + pub fn wait(self_: PyRef<'_, Self>) -> PyResult> { let inner = self_.inner.clone(); future_into_py(self_.py(), async move { @@ -44,3 +52,94 @@ impl Job { }) } } + +/// A row from `Connection.list_jobs`: one server-side job. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Clone)] +pub struct JobInfo { + job_id: String, + table: String, + job_type: String, + state: String, + created_at_millis: i64, +} + +#[pymethods] +impl JobInfo { + fn __repr__(&self) -> String { + format!( + "JobInfo(job_id={:?}, table={:?}, job_type={:?}, state={:?}, created_at_millis={})", + self.job_id, self.table, self.job_type, self.state, self.created_at_millis + ) + } +} + +impl From for JobInfo { + fn from(info: lancedb::database::JobInfo) -> Self { + Self { + job_id: info.job_id, + table: info.table, + job_type: info.job_type, + state: info.state, + created_at_millis: info.created_at_millis, + } + } +} + +/// The server's account of why a job failed. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Clone)] +pub struct JobFailureInfo { + phase: Option, + message: Option, + retryable: Option, +} + +#[pymethods] +impl JobFailureInfo { + fn __repr__(&self) -> String { + format!( + "JobFailureInfo(phase={:?}, message={:?}, retryable={:?})", + self.phase, self.message, self.retryable + ) + } +} + +/// A described job from `Connection.get_job`. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Clone)] +pub struct JobDescription { + job_id: String, + job_type: String, + state: String, + creation_ms: i64, + spec_json: Option, + failure: Option, +} + +#[pymethods] +impl JobDescription { + fn __repr__(&self) -> String { + format!( + "JobDescription(job_id={:?}, job_type={:?}, state={:?}, creation_ms={})", + self.job_id, self.job_type, self.state, self.creation_ms + ) + } +} + +impl From for JobDescription { + fn from(description: lancedb::database::JobDescription) -> Self { + Self { + job_id: description.job_id, + job_type: description.job_type, + state: description.state, + creation_ms: description.creation_ms, + spec_json: (!description.spec.is_null()).then(|| description.spec.to_string()), + failure: description.failure.map(|failure| JobFailureInfo { + phase: failure.phase, + message: failure.message, + retryable: failure.retryable, + }), + } + } +} diff --git a/python/src/lib.rs b/python/src/lib.rs index 6c9f62df9..6b0c0cf97 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -46,6 +46,9 @@ pub fn _lancedb(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::
()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/rust/lancedb/src/connection.rs b/rust/lancedb/src/connection.rs index f4ac0018b..89e59e12e 100644 --- a/rust/lancedb/src/connection.rs +++ b/rust/lancedb/src/connection.rs @@ -23,8 +23,8 @@ use crate::connection::create_table::CreateTableBuilder; use crate::data::scannable::Scannable; use crate::database::listing::ListingDatabase; use crate::database::{ - CloneTableRequest, Database, DatabaseOptions, OpenTableRequest, ReadConsistency, - TableNamesRequest, + CloneTableRequest, Database, DatabaseOptions, JobDescription, JobInfo, OpenTableRequest, + ReadConsistency, TableNamesRequest, }; use crate::embeddings::{EmbeddingRegistry, MemoryRegistry}; use crate::error::{Error, Result}; @@ -517,6 +517,39 @@ impl Connection { self.internal.read_consistency().await } + /// A [`crate::job::Job`] handle for a server-side job by id, suitable for + /// waiting on or cancelling the job. + /// + /// The handle is constructed without a server round trip; an unknown id + /// surfaces when the handle is used. Only server-backed databases support + /// job handles by id. + pub fn job(&self, job_id: impl AsRef) -> Result { + self.internal.job(job_id.as_ref()) + } + + /// List server-side jobs across the database's tables. + pub async fn list_jobs(&self) -> Result> { + self.internal.list_jobs().await + } + + /// Describe a single server-side job by id. `None` when the server has no + /// such job. + pub async fn get_job(&self, job_id: impl AsRef) -> Result> { + self.internal.get_job(job_id.as_ref()).await + } + + /// Request cancellation of a server-side job by id. Returns true if the + /// server accepted the cancellation, false if no such job exists. + pub async fn cancel_job(&self, job_id: impl AsRef) -> Result { + self.internal.cancel_job(job_id.as_ref()).await + } + + /// The lifecycle event history of a server-side job (all jobs when + /// `job_id` is `None`), as recorded Arrow batches. + pub async fn job_history(&self, job_id: Option<&str>) -> Result> { + self.internal.job_history(job_id).await + } + /// Drop a table in the database. /// /// # Arguments diff --git a/rust/lancedb/src/database.rs b/rust/lancedb/src/database.rs index 6ce5bdbc9..f99f6e12a 100644 --- a/rust/lancedb/src/database.rs +++ b/rust/lancedb/src/database.rs @@ -18,6 +18,8 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use arrow_array::RecordBatch; + use lance::dataset::ReadParams; use lance_namespace::LanceNamespace; use lance_namespace::models::{ @@ -200,6 +202,45 @@ pub enum ReadConsistency { Strong, } +/// A row from [`Database::list_jobs`]: one server-side job (index build, +/// compaction, column refresh, ...). +#[derive(Debug, Clone)] +pub struct JobInfo { + /// The job id -- what [`Database::get_job`] and [`Database::cancel_job`] + /// accept. + pub job_id: String, + /// The table the job runs against, without URI or namespace. + pub table: String, + pub job_type: String, + /// Lifecycle state: "running", "finished", "failed", or "cancelled". + pub state: String, + /// When the job was created, in milliseconds since the epoch. + pub created_at_millis: i64, +} + +/// A described job from [`Database::get_job`]: lifecycle state plus the +/// job-type-specific specification. +#[derive(Debug, Clone)] +pub struct JobDescription { + pub job_id: String, + pub job_type: String, + /// Lifecycle state: "running", "finished", "failed", or "cancelled". + pub state: String, + /// When the job was created, in milliseconds since the epoch. + pub creation_ms: i64, + /// The job-type-specific specification. Null when the server omits it. + pub spec: serde_json::Value, + /// Why the job failed, when the job is failed and the server reports a + /// reason. + pub failure: Option, +} + +fn job_op_not_supported(what: &str) -> Result { + Err(crate::error::Error::NotSupported { + message: format!("{} is not supported by this database", what), + }) +} + /// The `Database` trait defines the interface for database implementations. /// /// A database is responsible for managing tables and their metadata. @@ -245,6 +286,31 @@ pub trait Database: /// /// See [`CloneTableRequest`] for detailed documentation and examples. async fn clone_table(&self, request: CloneTableRequest) -> Result>; + /// A [`crate::job::Job`] handle for a server-side job by id, suitable for + /// waiting on or cancelling the job. The handle is constructed without a + /// server round trip; an unknown id surfaces when the handle is used. + fn job(&self, _job_id: &str) -> Result { + job_op_not_supported("job") + } + /// List server-side jobs across the database's tables. + async fn list_jobs(&self) -> Result> { + job_op_not_supported("list_jobs") + } + /// Describe a single job by id. `None` when the server has no such job. + async fn get_job(&self, _job_id: &str) -> Result> { + job_op_not_supported("get_job") + } + /// Request cancellation of a job by id. Returns true if the server + /// accepted the cancellation, false if no such job exists. Cancelling an + /// already-terminal job is a no-op success. + async fn cancel_job(&self, _job_id: &str) -> Result { + job_op_not_supported("cancel_job") + } + /// The lifecycle event history of a job (all jobs when `job_id` is + /// `None`), as recorded Arrow batches. + async fn job_history(&self, _job_id: Option<&str>) -> Result> { + job_op_not_supported("job_history") + } /// Open a table in the database async fn open_table(&self, request: OpenTableRequest) -> Result>; /// Rename a table in the database diff --git a/rust/lancedb/src/job.rs b/rust/lancedb/src/job.rs index 4c2c76d95..789ce8312 100644 --- a/rust/lancedb/src/job.rs +++ b/rust/lancedb/src/job.rs @@ -18,6 +18,7 @@ pub(crate) trait JobHandle: Send + Sync { fn id(&self) -> Option<&str> { None } + async fn status(&self) -> Result; async fn wait(&self) -> Result<()>; async fn cancel(&self) -> Result<()>; } @@ -65,6 +66,19 @@ impl Job { self.handle.as_ref().and_then(|handle| handle.id()) } + /// The operation's current lifecycle state: "running", "finished", + /// "failed", or "cancelled". + /// + /// A point snapshot; unlike [`Job::wait`] it does not block, raise on a + /// terminal failure state, or retry. States a newer server reports that + /// this client version does not know pass through as-is. + pub async fn status(&self) -> Result { + match &self.handle { + None => Ok("finished".to_string()), + Some(handle) => handle.status().await, + } + } + /// Waits until the operation reaches a terminal state. /// /// Returns [`crate::Error::JobFailed`] if the operation failed and @@ -138,6 +152,16 @@ impl SpawnedJob { #[async_trait] impl JobHandle for SpawnedJob { + async fn status(&self) -> Result { + let label = match &*self.outcome.borrow() { + None => "running", + Some(Outcome::Succeeded) => "finished", + Some(Outcome::Failed(_)) => "failed", + Some(Outcome::Cancelled) => "cancelled", + }; + Ok(label.to_string()) + } + async fn wait(&self) -> Result<()> { let mut outcome = self.outcome.clone(); let settled = outcome diff --git a/rust/lancedb/src/remote/db.rs b/rust/lancedb/src/remote/db.rs index a67f9dbb3..839cb3797 100644 --- a/rust/lancedb/src/remote/db.rs +++ b/rust/lancedb/src/remote/db.rs @@ -20,7 +20,7 @@ use lance_namespace::models::{ use crate::Error; use crate::database::{ CloneTableRequest, CreateTableMode, CreateTableRequest, Database, DatabaseOptions, - OpenTableRequest, ReadConsistency, TableNamesRequest, + JobDescription, JobInfo, OpenTableRequest, ReadConsistency, TableNamesRequest, }; use crate::error::Result; use crate::remote::util::stream_as_body; @@ -432,6 +432,73 @@ fn build_cache_key(name: &str, namespace: &[String]) -> String { key.iter().map(|b| format!("{:02x}", b)).collect() } +#[derive(serde::Deserialize)] +struct RemoteListJobRow { + job_id: String, + #[serde(default)] + table: String, + #[serde(default)] + job_type: String, + #[serde(default)] + state: String, + #[serde(default)] + created_at_millis: i64, +} + +#[derive(serde::Deserialize)] +struct RemoteListJobsResponse { + #[serde(default)] + jobs: Vec, + #[serde(default)] + page_token: Option, +} + +/// The server's account of why a job failed. Absent from older servers, +/// which report only the terminal state. +#[derive(serde::Deserialize)] +struct RemoteReportedFailure { + #[serde(default)] + phase: Option, + #[serde(default)] + message: Option, + #[serde(default)] + retryable: Option, +} + +#[derive(serde::Deserialize)] +struct RemoteDescribeJobResponse { + job_id: String, + #[serde(default)] + job_type: String, + job_state: String, + #[serde(default)] + creation_ms: i64, + #[serde(default)] + spec: serde_json::Value, + #[serde(default)] + failure: Option, +} + +/// Server job states -> the client vocabulary ("running" / "finished" / +/// "failed" / "cancelled"). Covers both the describe enum (IN_PROGRESS / +/// DONE / FAILED / CANCELLED) and the registry's lowercase list-row states +/// (in_progress / succeeded / failed / canceled / timed_out). States this +/// client version does not know (e.g. created, queued) pass through as-is. +fn job_state_to_client(state: &str) -> String { + match state { + "IN_PROGRESS" | "in_progress" => "running", + "DONE" | "done" | "succeeded" => "finished", + "FAILED" | "failed" | "TIMED_OUT" | "timed_out" => "failed", + "CANCELLED" | "cancelled" | "canceled" => "cancelled", + other => other, + } + .to_string() +} + +/// Bound on `list_jobs` page walking; a warning is logged when the listing +/// is truncated at this many pages. +const MAX_LIST_JOBS_PAGES: usize = 100; + #[async_trait] impl Database for RemoteDatabase { fn uri(&self) -> &str { @@ -445,6 +512,108 @@ impl Database for RemoteDatabase { }) } + fn job(&self, job_id: &str) -> Result { + Ok(crate::job::Job::new(Box::new(super::job::RemoteJob::new( + self.client.clone(), + job_id.to_string(), + )))) + } + + async fn list_jobs(&self) -> Result> { + let mut out = Vec::new(); + let mut page_token: Option = None; + for page in 0..MAX_LIST_JOBS_PAGES { + let mut body = serde_json::json!({}); + if let Some(token) = &page_token { + body["page_token"] = serde_json::Value::String(token.clone()); + } + let req = self.client.post("/v1/jobs/list").json(&body); + let (request_id, rsp) = self.client.send(req).await?; + let rsp = self.client.check_response(&request_id, rsp).await?; + let body: RemoteListJobsResponse = rsp.json().await.err_to_http(request_id)?; + out.extend(body.jobs.into_iter().map(|row| JobInfo { + job_id: row.job_id, + table: row.table, + job_type: row.job_type, + state: job_state_to_client(&row.state), + created_at_millis: row.created_at_millis, + })); + page_token = body.page_token; + if page_token.is_none() { + break; + } + if page + 1 == MAX_LIST_JOBS_PAGES { + log::warn!( + "list_jobs truncated after {} pages ({} jobs)", + MAX_LIST_JOBS_PAGES, + out.len() + ); + } + } + Ok(out) + } + + async fn get_job(&self, job_id: &str) -> Result> { + let req = self + .client + .post("/v1/jobs/describe") + .json(&serde_json::json!({ "job_id": job_id })); + let (request_id, rsp) = self.client.send(req).await?; + let rsp = match self.client.check_response(&request_id, rsp).await { + Ok(rsp) => rsp, + Err(Error::Http { + status_code: Some(StatusCode::NOT_FOUND), + .. + }) => return Ok(None), + Err(err) => return Err(err), + }; + let body: RemoteDescribeJobResponse = rsp.json().await.err_to_http(request_id)?; + Ok(Some(JobDescription { + job_id: body.job_id, + job_type: body.job_type, + state: job_state_to_client(&body.job_state), + creation_ms: body.creation_ms, + spec: body.spec, + failure: body.failure.map(|reported| crate::error::JobFailure { + phase: reported.phase, + message: reported.message, + retryable: reported.retryable, + source: None, + }), + })) + } + + async fn cancel_job(&self, job_id: &str) -> Result { + let req = self + .client + .post("/v1/jobs/cancel") + .json(&serde_json::json!({ "job_id": job_id })); + let (request_id, rsp) = self.client.send(req).await?; + match self.client.check_response(&request_id, rsp).await { + Ok(_) => Ok(true), + Err(Error::Http { + status_code: Some(StatusCode::NOT_FOUND), + .. + }) => Ok(false), + Err(err) => Err(err), + } + } + + async fn job_history(&self, job_id: Option<&str>) -> Result> { + let mut body = serde_json::json!({}); + if let Some(job_id) = job_id { + body["job_id"] = serde_json::Value::String(job_id.to_string()); + } + let req = self.client.post("/v1/jobs/query_events").json(&body); + let (request_id, rsp) = self.client.send(req).await?; + let rsp = self.client.check_response(&request_id, rsp).await?; + let bytes = rsp.bytes().await.err_to_http(request_id)?; + let reader = arrow_ipc::reader::StreamReader::try_new(std::io::Cursor::new(bytes), None)?; + reader + .collect::, _>>() + .map_err(Into::into) + } + async fn table_names(&self, request: TableNamesRequest) -> Result> { let mut req = if !request.namespace_path.is_empty() { let namespace_id = @@ -2094,4 +2263,165 @@ mod tests { assert!(list_response.tables.contains(&"table3".to_string())); } } + + #[tokio::test] + async fn test_list_jobs_paginates() { + let page = Arc::new(AtomicUsize::new(0)); + let conn = Connection::new_with_handler(move |request| { + assert_eq!(request.method(), &reqwest::Method::POST); + assert_eq!(request.url().path(), "/v1/jobs/list"); + let body: serde_json::Value = + serde_json::from_slice(request.body().unwrap().as_bytes().unwrap()).unwrap(); + match page.fetch_add(1, Ordering::SeqCst) { + 0 => { + assert!(body.get("page_token").is_none()); + http::Response::builder() + .status(200) + .body( + r#"{"jobs": [{"job_id": "job-1", "table": "t1", "job_type": "create_index", "state": "in_progress", "created_at_millis": 1000}], "page_token": "next"}"#, + ) + .unwrap() + } + _ => { + assert_eq!(body["page_token"], "next"); + http::Response::builder() + .status(200) + .body( + r#"{"jobs": [{"job_id": "job-2", "table": "t2", "job_type": "create_index", "state": "succeeded", "created_at_millis": 2000}, {"job_id": "job-3", "table": "t3", "job_type": "create_index", "state": "timed_out", "created_at_millis": 3000}]}"#, + ) + .unwrap() + } + } + }); + let jobs = conn.list_jobs().await.unwrap(); + assert_eq!(jobs.len(), 3); + assert_eq!(jobs[0].job_id, "job-1"); + assert_eq!(jobs[0].table, "t1"); + assert_eq!(jobs[0].state, "running"); + assert_eq!(jobs[1].job_id, "job-2"); + assert_eq!(jobs[1].state, "finished"); + assert_eq!(jobs[1].created_at_millis, 2000); + assert_eq!(jobs[2].job_id, "job-3"); + assert_eq!(jobs[2].state, "failed"); + } + + #[tokio::test] + async fn test_get_job() { + let conn = Connection::new_with_handler(|request| { + assert_eq!(request.method(), &reqwest::Method::POST); + assert_eq!(request.url().path(), "/v1/jobs/describe"); + let body: serde_json::Value = + serde_json::from_slice(request.body().unwrap().as_bytes().unwrap()).unwrap(); + assert_eq!(body["job_id"], "job-1"); + http::Response::builder() + .status(200) + .body( + r#"{"job_id": "job-1", "job_type": "create_index", "job_state": "FAILED", "creation_ms": 1000, "spec": {"column": "vec"}, "failure": {"phase": "execute", "message": "worker died", "retryable": true}}"#, + ) + .unwrap() + }); + let job = conn.get_job("job-1").await.unwrap().unwrap(); + assert_eq!(job.job_id, "job-1"); + assert_eq!(job.job_type, "create_index"); + assert_eq!(job.state, "failed"); + assert_eq!(job.creation_ms, 1000); + assert_eq!(job.spec["column"], "vec"); + let failure = job.failure.unwrap(); + assert_eq!(failure.phase.as_deref(), Some("execute")); + assert_eq!(failure.message.as_deref(), Some("worker died")); + assert_eq!(failure.retryable, Some(true)); + } + + #[tokio::test] + async fn test_get_job_missing_is_none() { + let conn = Connection::new_with_handler(|_| { + http::Response::builder() + .status(404) + .body("no such job") + .unwrap() + }); + assert!(conn.get_job("nope").await.unwrap().is_none()); + } + + #[tokio::test] + async fn test_cancel_job() { + let conn = Connection::new_with_handler(|request| { + assert_eq!(request.url().path(), "/v1/jobs/cancel"); + http::Response::builder() + .status(200) + .body(r#"{"job_id": "job-1"}"#) + .unwrap() + }); + assert!(conn.cancel_job("job-1").await.unwrap()); + + let conn = Connection::new_with_handler(|_| { + http::Response::builder() + .status(404) + .body("no such job") + .unwrap() + }); + assert!(!conn.cancel_job("nope").await.unwrap()); + } + + #[tokio::test] + async fn test_job_history_parses_arrow_stream() { + let schema = Arc::new(Schema::new(vec![Field::new( + "state", + DataType::Utf8, + false, + )])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::StringArray::from(vec![ + "created", "done", + ]))], + ) + .unwrap(); + let mut body = Vec::new(); + { + let mut writer = arrow_ipc::writer::StreamWriter::try_new(&mut body, &schema).unwrap(); + writer.write(&batch).unwrap(); + writer.finish().unwrap(); + } + let conn = Connection::new_with_handler(move |request| { + assert_eq!(request.url().path(), "/v1/jobs/query_events"); + let req_body: serde_json::Value = + serde_json::from_slice(request.body().unwrap().as_bytes().unwrap()).unwrap(); + assert_eq!(req_body["job_id"], "job-1"); + http::Response::builder() + .status(200) + .body(body.clone()) + .unwrap() + }); + let batches = conn.job_history(Some("job-1")).await.unwrap(); + assert_eq!(batches.len(), 1); + assert_eq!(batches[0].num_rows(), 2); + } + + #[tokio::test] + async fn test_conn_job_waits_to_done() { + let polls = Arc::new(AtomicUsize::new(0)); + let polls_ref = polls.clone(); + let conn = Connection::new_with_handler(move |request| { + assert_eq!(request.url().path(), "/v1/jobs/describe"); + let state = if polls_ref.fetch_add(1, Ordering::SeqCst) == 0 { + "IN_PROGRESS" + } else { + "DONE" + }; + http::Response::builder() + .status(200) + .body(format!( + r#"{{"job_id": "job-1", "job_type": "create_index", "job_state": "{}", "creation_ms": 1}}"#, + state + )) + .unwrap() + }); + let job = conn.job("job-1").unwrap(); + assert_eq!(job.id(), Some("job-1")); + assert_eq!(job.status().await.unwrap(), "running"); + job.wait().await.unwrap(); + assert_eq!(job.status().await.unwrap(), "finished"); + assert!(polls.load(Ordering::SeqCst) >= 3); + } } diff --git a/rust/lancedb/src/remote/job.rs b/rust/lancedb/src/remote/job.rs index 26bbfbbf9..2fc99da59 100644 --- a/rust/lancedb/src/remote/job.rs +++ b/rust/lancedb/src/remote/job.rs @@ -35,12 +35,28 @@ impl<'de> Deserialize<'de> for JobState { } } +impl JobState { + /// The client vocabulary label for this state. + fn client_label(&self) -> String { + match self { + Self::InProgress => "running".to_string(), + Self::Done => "finished".to_string(), + Self::Failed => "failed".to_string(), + Self::Cancelled => "cancelled".to_string(), + Self::Other(state) => state.clone(), + } + } +} + impl From<&str> for JobState { fn from(state: &str) -> Self { match state { "IN_PROGRESS" => Self::InProgress, "CANCELLED" => Self::Cancelled, - "FAILED" => Self::Failed, + // The server reports a timed-out job as FAILED on describe; + // accept the raw registry state too in case a future server + // stops folding it. + "FAILED" | "TIMED_OUT" => Self::Failed, "DONE" => Self::Done, other => Self::Other(other.to_string()), } @@ -51,9 +67,12 @@ impl From<&str> for JobState { /// report only the terminal state. #[derive(Deserialize)] struct ReportedFailure { - phase: String, - message: String, - retryable: bool, + #[serde(default)] + phase: Option, + #[serde(default)] + message: Option, + #[serde(default)] + retryable: Option, } #[derive(Deserialize)] @@ -98,6 +117,10 @@ impl JobHandle for RemoteJob { Some(&self.job_id) } + async fn status(&self) -> Result { + Ok(self.describe().await?.job_state.client_label()) + } + async fn wait(&self) -> Result<()> { let mut interval = INITIAL_POLL_INTERVAL; loop { @@ -110,9 +133,9 @@ impl JobHandle for RemoteJob { failure: description .failure .map(|reported| JobFailure { - phase: Some(reported.phase), - message: Some(reported.message), - retryable: Some(reported.retryable), + phase: reported.phase, + message: reported.message, + retryable: reported.retryable, source: None, }) .unwrap_or_default(), From 05602ec7d563d08c063bf4e36c087911b39af138 Mon Sep 17 00:00:00 2001 From: LuQQiu Date: Fri, 31 Jul 2026 16:16:17 -0700 Subject: [PATCH 25/29] chore: update lance dependency to v10.1.0-beta.1 (#3757) Updates the Lance Rust workspace dependencies and Java lance-core version to v10.1.0-beta.1. Includes a compatibility fix for the Lance file writer API by using the explicit V2_1 writer creation path for permutation shuffle spill files. Triggered by https://github.com/lance-format/lance/releases/tag/v10.1.0-beta.1 --- Cargo.lock | 91 +++++++++---------- Cargo.toml | 28 +++--- java/pom.xml | 2 +- .../src/dataloader/permutation/shuffle.rs | 12 ++- 4 files changed, 67 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76a15794b..57766d7e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3421,8 +3421,8 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsst" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow-array", "rand 0.9.5", @@ -4777,8 +4777,8 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" [[package]] name = "lance" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arc-swap", "arrow", @@ -4852,8 +4852,8 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow-array", "arrow-buffer", @@ -4875,7 +4875,7 @@ dependencies = [ [[package]] name = "lance-arrow-scalar" version = "58.0.0" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow-array", "arrow-buffer", @@ -4889,7 +4889,7 @@ dependencies = [ [[package]] name = "lance-arrow-stats" version = "58.0.0" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow-array", "arrow-schema", @@ -4898,8 +4898,8 @@ dependencies = [ [[package]] name = "lance-bitpacking" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrayref", "crunchy", @@ -4909,8 +4909,8 @@ dependencies = [ [[package]] name = "lance-core" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow-array", "arrow-buffer", @@ -4950,8 +4950,8 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow", "arrow-array", @@ -4981,8 +4981,8 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow", "arrow-array", @@ -4999,8 +4999,8 @@ dependencies = [ [[package]] name = "lance-derive" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "proc-macro2", "quote", @@ -5009,8 +5009,8 @@ dependencies = [ [[package]] name = "lance-encoding" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow-arith", "arrow-array", @@ -5045,12 +5045,13 @@ dependencies = [ [[package]] name = "lance-file" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow-arith", "arrow-array", "arrow-buffer", + "arrow-cast", "arrow-data", "arrow-schema", "arrow-select", @@ -5076,8 +5077,8 @@ dependencies = [ [[package]] name = "lance-index" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arc-swap", "arrow", @@ -5144,8 +5145,8 @@ dependencies = [ [[package]] name = "lance-index-core" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow-array", "arrow-schema", @@ -5167,18 +5168,12 @@ dependencies = [ [[package]] name = "lance-io" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow", - "arrow-arith", "arrow-array", - "arrow-buffer", - "arrow-cast", - "arrow-data", "arrow-schema", - "arrow-select", - "async-recursion", "async-trait", "aws-config", "aws-credential-types", @@ -5211,8 +5206,8 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow-array", "arrow-buffer", @@ -5228,8 +5223,8 @@ dependencies = [ [[package]] name = "lance-namespace" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow", "async-trait", @@ -5241,8 +5236,8 @@ dependencies = [ [[package]] name = "lance-namespace-impls" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow", "arrow-ipc", @@ -5296,8 +5291,8 @@ dependencies = [ [[package]] name = "lance-select" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow-array", "arrow-buffer", @@ -5312,8 +5307,8 @@ dependencies = [ [[package]] name = "lance-table" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow", "arrow-array", @@ -5352,8 +5347,8 @@ dependencies = [ [[package]] name = "lance-testing" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "arrow-array", "arrow-schema", @@ -5366,8 +5361,8 @@ dependencies = [ [[package]] name = "lance-tokenizer" -version = "10.0.0-beta.7" -source = "git+https://github.com/lance-format/lance.git?tag=v10.0.0-beta.7#c164cdf7cedbcb04ab1c44a6101722a33cfa9232" +version = "10.1.0-beta.1" +source = "git+https://github.com/lance-format/lance.git?tag=v10.1.0-beta.1#68f4d4c1d0c4871b067557c61fc405078f1ab3b7" dependencies = [ "icu_segmenter", "jieba-rs", diff --git a/Cargo.toml b/Cargo.toml index 9638da17f..35370e58f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,20 +13,20 @@ categories = ["database-implementations"] rust-version = "1.91.0" [workspace.dependencies] -lance = { "version" = "=10.0.0-beta.7", default-features = false, "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-core = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-datagen = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-file = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-io = { "version" = "=10.0.0-beta.7", default-features = false, "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-index = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-linalg = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-namespace = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-namespace-impls = { "version" = "=10.0.0-beta.7", default-features = false, "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-table = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-testing = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-datafusion = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-encoding = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } -lance-arrow = { "version" = "=10.0.0-beta.7", "tag" = "v10.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } +lance = { "version" = "=10.1.0-beta.1", default-features = false, "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-core = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-datagen = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-file = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-io = { "version" = "=10.1.0-beta.1", default-features = false, "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-index = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-linalg = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-namespace = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-namespace-impls = { "version" = "=10.1.0-beta.1", default-features = false, "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-table = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-testing = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-datafusion = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-encoding = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } +lance-arrow = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" } ahash = "0.8" # Note that this one does not include pyarrow arrow = { version = "58.0.0", optional = false } diff --git a/java/pom.xml b/java/pom.xml index 7e5194159..1b96cbcab 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -28,7 +28,7 @@ UTF-8 15.0.0 - 10.0.0-beta.7 + 10.1.0-beta.1 false 2.30.0 1.7 diff --git a/rust/lancedb/src/dataloader/permutation/shuffle.rs b/rust/lancedb/src/dataloader/permutation/shuffle.rs index cf118a996..2bc06452c 100644 --- a/rust/lancedb/src/dataloader/permutation/shuffle.rs +++ b/rust/lancedb/src/dataloader/permutation/shuffle.rs @@ -11,7 +11,9 @@ use lance_core::{cache::LanceCache, utils::futures::FinallyStreamExt}; use lance_encoding::decoder::{DecoderPlugins, FilterExpression}; use lance_file::{ reader::{FileReader, FileReaderOptions}, - writer::{FileWriter, FileWriterOptions}, + version::ConcreteFileVersion, + versions, + writer::FileWriterOptions, }; use lance_io::{ ReadBatchParams, @@ -152,8 +154,12 @@ impl Shuffler { source: None, })?; let object_writer = object_store.create(&path).await?; - let writer = - FileWriter::try_new(object_writer, schema.clone(), FileWriterOptions::default())?; + let writer = versions::create_writer( + ConcreteFileVersion::V2_1, + object_writer, + schema.clone(), + FileWriterOptions::default(), + )?; file_writers.push(writer); } From 93354baf34ebe4af46aebd50ea02d27f44abc4e3 Mon Sep 17 00:00:00 2001 From: Will Jones Date: Fri, 31 Jul 2026 16:40:58 -0700 Subject: [PATCH 26/29] chore: upgrade rust toolchain to 1.97.0 (#3643) Bumps the pinned Rust toolchain from 1.95.0 to the latest stable (1.97.0). Rust 1.97's clippy adds `useless_borrows_in_formatting`, which flags a redundant `&` in `format!`/`debug!` arguments in a few places. This PR removes those to keep `cargo clippy` clean. No behavior change; the MSRV (`rust-version = "1.91.0"`) is unchanged. Co-authored-by: Claude Opus 4.8 (1M context) --- rust-toolchain.toml | 2 +- rust/lancedb/src/embeddings.rs | 6 +++--- rust/lancedb/src/remote/client.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f25b5b140..398f3f015 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.95.0" +channel = "1.97.0" diff --git a/rust/lancedb/src/embeddings.rs b/rust/lancedb/src/embeddings.rs index 58e037bdd..469c25b4a 100644 --- a/rust/lancedb/src/embeddings.rs +++ b/rust/lancedb/src/embeddings.rs @@ -264,7 +264,7 @@ pub fn compute_output_schema( let field_name = ed .dest_column .clone() - .unwrap_or_else(|| format!("{}_embedding", &ed.source_column)); + .unwrap_or_else(|| format!("{}_embedding", ed.source_column)); sb.push(Field::new( field_name, @@ -291,7 +291,7 @@ pub fn compute_embeddings_for_batch( let dst_field_name = fld .dest_column .clone() - .unwrap_or_else(|| format!("{}_embedding", &fld.source_column)); + .unwrap_or_else(|| format!("{}_embedding", fld.source_column)); let dst_field = Field::new( dst_field_name, @@ -315,7 +315,7 @@ impl WithEmbeddings { let field_name = ed .dest_column .clone() - .unwrap_or_else(|| format!("{}_embedding", &ed.source_column)); + .unwrap_or_else(|| format!("{}_embedding", ed.source_column)); Ok(Field::new( field_name, func.dest_type()?.into_owned(), diff --git a/rust/lancedb/src/remote/client.rs b/rust/lancedb/src/remote/client.rs index 2f4ac03d3..9e34fca9f 100644 --- a/rust/lancedb/src/remote/client.rs +++ b/rust/lancedb/src/remote/client.rs @@ -706,7 +706,7 @@ impl RestfulLanceDbClient { .err_to_http(request_id.clone())?; debug!( "Received response for request_id={}: {:?}", - request_id, &response + request_id, response ); Ok((request_id, response)) } @@ -768,7 +768,7 @@ impl RestfulLanceDbClient { Ok((status, response)) if status.is_success() => { debug!( "Received response for request_id={}: {:?}", - retry_counter.request_id, &response + retry_counter.request_id, response ); return Ok((retry_counter.request_id, response)); } From 9e26bf3fba7b77bb32434c0f6af9dcb43248f90a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 09:53:09 -0700 Subject: [PATCH 27/29] chore(deps): bump the rust-minor-patch group with 3 updates (#3758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the rust-minor-patch group with 3 updates: [http](https://github.com/hyperium/http), [napi-derive](https://github.com/napi-rs/napi-rs) and [napi-build](https://github.com/napi-rs/napi-rs). Updates `http` from 1.4.2 to 1.5.0
Release notes

Sourced from http's releases.

v1.5.0

What's Changed

New Contributors

Full Changelog: https://github.com/hyperium/http/compare/v1.4.2...v1.5.0

Changelog

Sourced from http's changelog.

1.5.0 (July 29, 2026)

  • Add Method::QUERY constant for the new QUERY method defined in RFC 10008.
  • Fix uri::Builder::path_and_query() to allow empty strings to mean no path.
  • Fix uri::PathAndQuery parsing to enforce URI max length.
Commits

Updates `napi-derive` from 3.6.0 to 3.6.1
Release notes

Sourced from napi-derive's releases.

napi-derive-v3.6.1

Other

  • updated the following local packages: napi-derive-backend
Commits
  • 58bd87f chore: release (#3414)
  • 9da8723 chore(release): publish
  • 8d22196 chore(deps): update dependency oxc-parser to ^0.142.0 (#3422)
  • abc30fb build(deps): bump postcss from 8.5.17 to 8.5.23 (#3421)
  • 5542139 build(deps): bump fast-xml-parser from 5.9.3 to 5.10.1 (#3418)
  • dc4ee8c build(deps): bump fast-uri from 3.1.3 to 3.1.4 (#3419)
  • 050d985 feat(async-runtime): drain-linger surface + lock-free scheduler internals (#3...
  • e0b8708 chore(deps): update dependency oxc-parser to ^0.141.0 (#3417)
  • fc84940 chore(deps): update actions/setup-node action to v7 (#3413)
  • ee598db build(deps): bump protobufjs from 7.6.4 to 7.6.5 (#3410)
  • Additional commits viewable in compare view

Updates `napi-build` from 2.3.2 to 2.4.0
Release notes

Sourced from napi-build's releases.

napi-build-v2.4.0

Added

  • (cli) support non-threaded WASI targets (#3353)
Commits
  • 58bd87f chore: release (#3414)
  • 9da8723 chore(release): publish
  • 8d22196 chore(deps): update dependency oxc-parser to ^0.142.0 (#3422)
  • abc30fb build(deps): bump postcss from 8.5.17 to 8.5.23 (#3421)
  • 5542139 build(deps): bump fast-xml-parser from 5.9.3 to 5.10.1 (#3418)
  • dc4ee8c build(deps): bump fast-uri from 3.1.3 to 3.1.4 (#3419)
  • 050d985 feat(async-runtime): drain-linger surface + lock-free scheduler internals (#3...
  • e0b8708 chore(deps): update dependency oxc-parser to ^0.141.0 (#3417)
  • fc84940 chore(deps): update actions/setup-node action to v7 (#3413)
  • ee598db build(deps): bump protobufjs from 7.6.4 to 7.6.5 (#3410)
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 124 ++++++++++++++++++++++++++--------------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57766d7e9..25586e622 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,7 +601,7 @@ dependencies = [ "bytes", "fastrand", "hex", - "http 1.4.2", + "http 1.5.0", "sha1 0.10.6", "time", "tokio", @@ -664,7 +664,7 @@ dependencies = [ "bytes-utils", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "http-body 0.4.6", "http-body 1.1.0", "percent-encoding", @@ -694,7 +694,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "http-body-util", "regex-lite", "tracing", @@ -719,7 +719,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "regex-lite", "tracing", ] @@ -743,7 +743,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "regex-lite", "tracing", ] @@ -773,7 +773,7 @@ dependencies = [ "hex", "hmac 0.13.0", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "lru", "percent-encoding", @@ -802,7 +802,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "regex-lite", "tracing", ] @@ -826,7 +826,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "regex-lite", "tracing", ] @@ -851,7 +851,7 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "regex-lite", "tracing", ] @@ -873,7 +873,7 @@ dependencies = [ "hex", "hmac 0.13.0", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "p256", "percent-encoding", "ring", @@ -906,7 +906,7 @@ dependencies = [ "bytes", "crc-fast", "hex", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "md-5 0.11.0", @@ -940,7 +940,7 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "percent-encoding", @@ -961,7 +961,7 @@ dependencies = [ "h2 0.3.27", "h2 0.4.14", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "http-body 0.4.6", "hyper 0.14.32", "hyper 1.9.0", @@ -1023,7 +1023,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "http-body 0.4.6", "http-body 1.1.0", "http-body-util", @@ -1044,7 +1044,7 @@ dependencies = [ "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "pin-project-lite", "tokio", "tracing", @@ -1070,7 +1070,7 @@ checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", - "http 1.4.2", + "http 1.5.0", ] [[package]] @@ -1084,7 +1084,7 @@ dependencies = [ "bytes-utils", "futures-core", "http 0.2.12", - "http 1.4.2", + "http 1.5.0", "http-body 0.4.6", "http-body 1.1.0", "http-body-util", @@ -1132,7 +1132,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "hyper 1.9.0", @@ -1165,7 +1165,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "mime", @@ -3842,7 +3842,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.2", + "http 1.5.0", "indexmap 2.14.0", "slab", "tokio", @@ -3953,7 +3953,7 @@ checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" dependencies = [ "dirs", "futures", - "http 1.4.2", + "http 1.5.0", "indicatif", "libc", "log", @@ -3976,7 +3976,7 @@ checksum = "430b33fa84f92796d4d263070b6c0d3ca219df7b9a0e1853ee431029b1612bcd" dependencies = [ "async-trait", "bytes", - "http 1.4.2", + "http 1.5.0", "more-asserts", "serde", "thiserror 2.0.18", @@ -4041,9 +4041,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -4067,7 +4067,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", - "http 1.4.2", + "http 1.5.0", ] [[package]] @@ -4078,7 +4078,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "pin-project-lite", ] @@ -4145,7 +4145,7 @@ dependencies = [ "futures-channel", "futures-core", "h2 0.4.14", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "httparse", "httpdate", @@ -4177,7 +4177,7 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.2", + "http 1.5.0", "hyper 1.9.0", "hyper-util", "rustls 0.23.40", @@ -4211,7 +4211,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "hyper 1.9.0", "ipnet", @@ -5182,7 +5182,7 @@ dependencies = [ "chrono", "futures", "goosefs-sdk", - "http 1.4.2", + "http 1.5.0", "io-uring", "lance-arrow", "lance-core", @@ -5414,7 +5414,7 @@ dependencies = [ "goosefs-sdk", "half", "hf-hub", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "lance", "lance-arrow", @@ -6080,15 +6080,15 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.3.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" +checksum = "5282704fbe8d49b0cf8b08e3f33233416a528658f205c7e5ace63b582de0b11c" [[package]] name = "napi-derive" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a49c513341a61a16a10af6efcce46b30d0822ba2d4fb197d24d33dfc199c78d5" +checksum = "4d5c9c02556ea6dc99dffd36c1ce60141411657438501a125b675776d011ce92" dependencies = [ "convert_case", "ctor 1.0.5", @@ -6100,9 +6100,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "6.0.0" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4747005fa3e2c9989ac45a723a514c5db2411238b72981a3cda4c701a9dfea17" +checksum = "d60b5d773ad46c698c8cc2cd9fde0b283d39cbb7f71c04bee633c7bdba4423bd" dependencies = [ "convert_case", "proc-macro2", @@ -6356,7 +6356,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body-util", "httparse", "humantime", @@ -6477,7 +6477,7 @@ dependencies = [ "base64 0.22.1", "bytes", "futures", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "jiff", "log", @@ -6502,7 +6502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6f81ba6960e3fae1882f253b114b21d7e444e1534f209c7737a79f6243eb6f" dependencies = [ "futures", - "http 1.4.2", + "http 1.5.0", "mea", "opendal-core", ] @@ -6546,7 +6546,7 @@ checksum = "0030644366ef5d8cbe3a4a5822bf99a4aafddc1666e9d24b44d158d9062fc76a" dependencies = [ "base64 0.22.1", "bytes", - "http 1.4.2", + "http 1.5.0", "log", "opendal-core", "opendal-service-azure-common", @@ -6567,7 +6567,7 @@ checksum = "6dea4908d490143a9b0b7f7a790e139ff829b06a023f670455ed3d44f664b361" dependencies = [ "base64 0.22.1", "bytes", - "http 1.4.2", + "http 1.5.0", "log", "opendal-core", "opendal-service-azure-common", @@ -6585,7 +6585,7 @@ version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b489f13c42e69d69bdd72952b634356ec43a7881a20259b38b540fcecdf4051" dependencies = [ - "http 1.4.2", + "http 1.5.0", "opendal-core", ] @@ -6596,7 +6596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa8cafe9729213375c7331019b0cb756ad3e1aff7f45cd32c45eae91ebde8901" dependencies = [ "bytes", - "http 1.4.2", + "http 1.5.0", "log", "opendal-core", "quick-xml 0.39.4", @@ -6614,7 +6614,7 @@ checksum = "48de101aac565ed06af4b47903c24eafd249075553ec1fb18256751c45148d47" dependencies = [ "async-trait", "bytes", - "http 1.4.2", + "http 1.5.0", "log", "opendal-core", "percent-encoding", @@ -6649,7 +6649,7 @@ checksum = "c4922661976a1d40794a2adfbdb888cc3c23097690f825a92f773af38908a848" dependencies = [ "bytes", "hf-xet", - "http 1.4.2", + "http 1.5.0", "log", "opendal-core", "percent-encoding", @@ -6665,7 +6665,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "328fa55e8888cbdfe00826bfea2a79042422b720e8369e9e021e46121dea5ace" dependencies = [ "bytes", - "http 1.4.2", + "http 1.5.0", "log", "opendal-core", "quick-xml 0.39.4", @@ -6684,7 +6684,7 @@ dependencies = [ "base64 0.22.1", "bytes", "crc32c", - "http 1.4.2", + "http 1.5.0", "log", "md-5 0.11.0", "opendal-core", @@ -8226,7 +8226,7 @@ checksum = "57ac2757f3140aa2e213b554148ae0b52733e624fc6723f0cc6bb3d440176c95" dependencies = [ "anyhow", "form_urlencoded", - "http 1.4.2", + "http 1.5.0", "log", "percent-encoding", "reqsign-core", @@ -8244,7 +8244,7 @@ dependencies = [ "anyhow", "bytes", "form_urlencoded", - "http 1.4.2", + "http 1.5.0", "log", "percent-encoding", "quick-xml 0.39.4", @@ -8266,7 +8266,7 @@ dependencies = [ "base64 0.22.1", "bytes", "form_urlencoded", - "http 1.4.2", + "http 1.5.0", "jsonwebtoken", "log", "pem", @@ -8291,7 +8291,7 @@ dependencies = [ "futures", "hex", "hmac 0.12.1", - "http 1.4.2", + "http 1.5.0", "jiff", "log", "percent-encoding", @@ -8318,7 +8318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35cc609b49c69e76ecaceb775a03f792d1ed3e7755ab3548d4534fd801e3242e" dependencies = [ "form_urlencoded", - "http 1.4.2", + "http 1.5.0", "jsonwebtoken", "log", "percent-encoding", @@ -8338,7 +8338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e128f19525861dbded59e1e7c17653a8ed63d573ca04aed708d552dbef5bb32a" dependencies = [ "anyhow", - "http 1.4.2", + "http 1.5.0", "log", "percent-encoding", "reqsign-core", @@ -8358,7 +8358,7 @@ dependencies = [ "futures-core", "futures-util", "h2 0.4.14", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "hyper 1.9.0", @@ -8402,7 +8402,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "hyper 1.9.0", @@ -8456,7 +8456,7 @@ checksum = "199dda04a536b532d0cc04d7979e39b1c763ea749bf91507017069c00b96056f" dependencies = [ "anyhow", "async-trait", - "http 1.4.2", + "http 1.5.0", "reqwest 0.13.3", "thiserror 2.0.18", "tower-service", @@ -10058,7 +10058,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2 0.4.14", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "hyper 1.9.0", @@ -10114,7 +10114,7 @@ checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "bitflags 2.11.1", "bytes", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "pin-project-lite", @@ -10134,7 +10134,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.2", + "http 1.5.0", "http-body 1.1.0", "http-body-util", "pin-project-lite", @@ -11155,7 +11155,7 @@ dependencies = [ "clap", "crc32fast", "futures", - "http 1.4.2", + "http 1.5.0", "hyper 1.9.0", "lazy_static", "more-asserts", @@ -11229,7 +11229,7 @@ dependencies = [ "chrono", "clap", "gearhash", - "http 1.4.2", + "http 1.5.0", "itertools 0.14.0", "lazy_static", "more-asserts", From 3dd9c598e9f0cd6004bd0bd30a67f9b279cef8c1 Mon Sep 17 00:00:00 2001 From: Drew Gallardo Date: Mon, 3 Aug 2026 08:38:08 -0700 Subject: [PATCH 28/29] feat(remote): add seekable blob range reads (#3750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Implements Cloud `fetch_blob_files`: returns real seekable `BlobFile` handles over HTTP Range instead of `NotSupported`. - Completes the second Cloud blob read verb after #3684 (`fetch_blobs` = eager whole bytes; this = lazy / partial / sequential reads). - Same public handle API as local (`read_range`, `read_up_to`, `seek`, `tell`, `close`), so one code path works for local and Cloud. Large blobs (video, audio, PDFs) should not require downloading the whole object to inspect a header or stream a slice. After search, callers open a handle and read only what they need: ```python hits = table.search(vec).select(["id", "video"]).limit(5).to_arrow() with table.fetch_blob_files("video", hits)[0] as f: header = f.read_range(0, 256) f.seek(keyframe_offset) chunk = f.read_up_to(1 << 20) ``` ### Behavior - Handle creation probes size with `bytes=0-0` (bounded concurrency, input order preserved). - `204` → null (`None`); `416` with `bytes */0` → valid empty blob; other `416` → error. - `read_range` validates `Content-Range` and body length; OOB ranges fail with `invalid_input` before the request (aligned with Lance). - `read_up_to` reuses one open-ended Range response across sequential reads; `seek` drops it. - Servers older than 0.5.0 get a clear `NotSupported` (does not suggest `fetch_blobs`, which they also lack). ## Testing - `cargo test --features remote -p lancedb remote_blob` - `cargo test --features remote -p lancedb test_blob` - `cargo clippy --features remote --tests --examples` (no new warnings from this change) --------- Co-authored-by: Claude Fable 5 Co-authored-by: Cursor --- python/python/lancedb/table.py | 4 +- python/python/tests/test_remote_db.py | 35 +- python/src/table.rs | 36 +- rust/lancedb/src/blob.rs | 200 +++- rust/lancedb/src/remote/table.rs | 27 +- rust/lancedb/src/remote/table/blobs.rs | 1225 +++++++++++++++++++++++- rust/lancedb/src/table.rs | 2 +- 7 files changed, 1471 insertions(+), 58 deletions(-) diff --git a/python/python/lancedb/table.py b/python/python/lancedb/table.py index ea0f97621..31a70c298 100644 --- a/python/python/lancedb/table.py +++ b/python/python/lancedb/table.py @@ -1595,8 +1595,8 @@ class Table(ABC): Prefer this over :meth:`fetch_blobs` for large payloads. ``row_ids`` is a ``list[int]`` or a query ``pyarrow.Table`` carrying row identity via ``_rowid`` or a ``_lance_row_id`` field on the blob descriptor. Null - rows are ``None``. Unsupported on LanceDB Cloud, where - :meth:`fetch_blobs` returns full bytes instead. + rows are ``None``. Remote tables require LanceDB Cloud server 0.5.0 or + newer. """ @abstractmethod diff --git a/python/python/tests/test_remote_db.py b/python/python/tests/test_remote_db.py index 757d8a758..d5d3569d3 100644 --- a/python/python/tests/test_remote_db.py +++ b/python/python/tests/test_remote_db.py @@ -2055,6 +2055,24 @@ def blob_remote_table(*, server_version=Version("0.5.0")): request.send_header("phalanx-version", str(server_version)) request.end_headers() request.wfile.write(json.dumps(BLOB_DESCRIBE_RESPONSE).encode()) + elif request.path.startswith("/v1/table/test/blob/image/"): + path = request.path.partition("?")[0] + row_id = int(path.split("/")[-2]) + payload = {10: b"alpha", 20: None, 30: b"gamma"}[row_id] + if payload is None: + request.send_response(204) + request.end_headers() + return + byte_range = request.headers["Range"].removeprefix("bytes=") + start_text, end_text = byte_range.split("-", maxsplit=1) + start = int(start_text) + end = int(end_text) if end_text else len(payload) - 1 + chunk = payload[start : end + 1] + request.send_response(206) + request.send_header("Content-Range", f"bytes {start}-{end}/{len(payload)}") + request.send_header("Content-Length", str(len(chunk))) + request.end_headers() + request.wfile.write(chunk) elif request.path == "/v1/table/test/query/": content_len = int(request.headers.get("Content-Length", 0)) body = json.loads(request.rfile.read(content_len)) @@ -2092,8 +2110,21 @@ def test_remote_blob_columns_and_fetch(): assert table.blob_columns() == ["image"] blobs = table.fetch_blobs("image", [10, 20, 30]) assert blobs.to_pylist() == [b"alpha", None, b"gamma"] - with pytest.raises(NotImplementedError, match="Use fetch_blobs for full bytes"): - table.fetch_blob_files("image", [10, 20, 30]) + + +def test_remote_blob_files_are_lazy_seekable_handles(): + with blob_remote_table() as table: + files = table.fetch_blob_files("image", [10, 20, 30]) + + assert len(files) == 3 + alpha, null_row, gamma = files + assert null_row is None + assert alpha is not None + assert gamma is not None + assert alpha.size() == 5 + assert alpha.read_range(1, 3) == b"lph" + gamma.seek(2) + assert gamma.read() == b"mma" def test_remote_blob_fetch_accepts_query_table(): diff --git a/python/src/table.rs b/python/src/table.rs index a4e50c183..d69609093 100644 --- a/python/src/table.rs +++ b/python/src/table.rs @@ -426,9 +426,11 @@ pub struct PyBlobFile { impl PyBlobFile { fn read_bytes(self_: PyRef<'_, Self>) -> PyResult> { let inner = self_.inner.clone(); - let bytes = block_on(async move { inner.read().await }) + let py = self_.py(); + let bytes = py + .detach(move || block_on(async move { inner.read().await })) .map_err(|e| PyRuntimeError::new_err(format!("blob read failed: {e}")))?; - Ok(PyBytes::new(self_.py(), bytes.as_ref()).unbind()) + Ok(PyBytes::new(py, bytes.as_ref()).unbind()) } pub fn read(self_: PyRef<'_, Self>) -> PyResult> { @@ -444,24 +446,32 @@ impl PyBlobFile { fn close(self_: PyRef<'_, Self>) -> PyResult<()> { let inner = self_.inner.clone(); - block_on(async move { inner.close().await }) + self_ + .py() + .detach(move || block_on(async move { inner.close().await })) .map_err(|e| PyRuntimeError::new_err(format!("blob close failed: {e}"))) } fn is_closed(self_: PyRef<'_, Self>) -> bool { let inner = self_.inner.clone(); - block_on(async move { inner.is_closed().await }) + self_ + .py() + .detach(move || block_on(async move { inner.is_closed().await })) } fn seek(self_: PyRef<'_, Self>, position: u64) -> PyResult<()> { let inner = self_.inner.clone(); - block_on(async move { inner.seek(position).await }) + self_ + .py() + .detach(move || block_on(async move { inner.seek(position).await })) .map_err(|e| PyRuntimeError::new_err(format!("blob seek failed: {e}"))) } fn tell(self_: PyRef<'_, Self>) -> PyResult { let inner = self_.inner.clone(); - block_on(async move { inner.tell().await }) + self_ + .py() + .detach(move || block_on(async move { inner.tell().await })) .map_err(|e| PyRuntimeError::new_err(format!("blob tell failed: {e}"))) } @@ -475,16 +485,20 @@ impl PyBlobFile { .checked_add(length as u64) .ok_or_else(|| PyValueError::new_err("offset + length overflowed"))?; let inner = self_.inner.clone(); - let bytes = block_on(async move { inner.read_range(offset..end).await }) + let py = self_.py(); + let bytes = py + .detach(move || block_on(async move { inner.read_range(offset..end).await })) .map_err(|e| PyRuntimeError::new_err(format!("blob read_range failed: {e}")))?; - Ok(PyBytes::new(self_.py(), bytes.as_ref()).unbind()) + Ok(PyBytes::new(py, bytes.as_ref()).unbind()) } fn read_up_to(self_: PyRef<'_, Self>, length: usize) -> PyResult> { let inner = self_.inner.clone(); - let bytes = block_on(async move { inner.read_up_to(length).await }) - .map_err(|e| PyRuntimeError::new_err(format!("blob read failed: {e}")))?; - Ok(PyBytes::new(self_.py(), bytes.as_ref()).unbind()) + let py = self_.py(); + let bytes = py + .detach(move || block_on(async move { inner.read_up_to(length).await })) + .map_err(|e| PyRuntimeError::new_err(format!("blob read_up_to failed: {e}")))?; + Ok(PyBytes::new(py, bytes.as_ref()).unbind()) } } diff --git a/rust/lancedb/src/blob.rs b/rust/lancedb/src/blob.rs index a21fe3c1b..3448257ab 100644 --- a/rust/lancedb/src/blob.rs +++ b/rust/lancedb/src/blob.rs @@ -9,6 +9,7 @@ //! //! Blob tables require Lance file format >= 2.2 and stable row ids at create. +use std::ops::Range; use std::sync::Arc; use arrow_array::LargeBinaryArray; @@ -17,10 +18,202 @@ use arrow_schema::{DataType, Field, Schema}; use lance::dataset::{BlobRangeRequest as LanceBlobRangeRequest, Dataset, WriteParams}; use lance_arrow::FieldExt; use lance_encoding::version::LanceFileVersion; +use lance_io::object_store::ObjectStore; +use object_store::path::Path; use crate::error::{Error, Result}; -pub use lance::dataset::BlobFile; +/// Seekable handle for one blob value, backed by local storage or a remote +/// HTTP byte-range endpoint. +#[derive(Debug)] +pub struct BlobFile { + inner: BlobFileInner, +} + +#[derive(Debug)] +enum BlobFileInner { + Native(lance::dataset::BlobFile), + #[cfg(feature = "remote")] + Remote(Box), +} + +impl From for BlobFile { + fn from(value: lance::dataset::BlobFile) -> Self { + Self { + inner: BlobFileInner::Native(value), + } + } +} + +#[cfg(feature = "remote")] +impl From for BlobFile { + fn from(value: crate::remote::table::blobs::RemoteBlobFile) -> Self { + Self { + inner: BlobFileInner::Remote(Box::new(value)), + } + } +} + +impl BlobFile { + /// Inline reader over a data-file slice. + pub fn new_inline( + object_store: Arc, + path: Path, + position: u64, + size: u64, + ) -> Self { + lance::dataset::BlobFile::new_inline(object_store, path, position, size).into() + } + + /// Dedicated sidecar-file reader. + pub fn new_dedicated(object_store: Arc, path: Path, size: u64) -> Self { + lance::dataset::BlobFile::new_dedicated(object_store, path, size).into() + } + + /// Packed reader for a slice in a shared sidecar. + pub fn new_packed( + object_store: Arc, + path: Path, + position: u64, + size: u64, + ) -> Self { + lance::dataset::BlobFile::new_packed(object_store, path, position, size).into() + } + + /// External reader at a resolved object location. + pub fn new_external( + object_store: Arc, + path: Path, + uri: String, + position: u64, + size: u64, + ) -> Self { + lance::dataset::BlobFile::new_external(object_store, path, uri, position, size).into() + } + + /// Close the handle. + pub async fn close(&self) -> lance_core::Result<()> { + match &self.inner { + BlobFileInner::Native(file) => file.close().await, + #[cfg(feature = "remote")] + BlobFileInner::Remote(file) => file.close().await, + } + } + + /// Whether the handle is closed. + pub async fn is_closed(&self) -> bool { + match &self.inner { + BlobFileInner::Native(file) => file.is_closed().await, + #[cfg(feature = "remote")] + BlobFileInner::Remote(file) => file.is_closed(), + } + } + + /// Read a range without moving the cursor. + pub async fn read_range(&self, range: Range) -> lance_core::Result { + match &self.inner { + BlobFileInner::Native(file) => file.read_range(range).await, + #[cfg(feature = "remote")] + BlobFileInner::Remote(file) => file.read_range(range).await, + } + } + + /// Read ranges without moving the cursor. + pub async fn read_ranges( + &self, + ranges: &[Range], + ) -> lance_core::Result> { + match &self.inner { + BlobFileInner::Native(file) => file.read_ranges(ranges).await, + #[cfg(feature = "remote")] + BlobFileInner::Remote(file) => file.read_ranges(ranges).await, + } + } + + /// Read from the cursor to the end. + pub async fn read(&self) -> lance_core::Result { + match &self.inner { + BlobFileInner::Native(file) => file.read().await, + #[cfg(feature = "remote")] + BlobFileInner::Remote(file) => file.read().await, + } + } + + /// Read up to `len` bytes and advance the cursor. + pub async fn read_up_to(&self, len: usize) -> lance_core::Result { + match &self.inner { + BlobFileInner::Native(file) => file.read_up_to(len).await, + #[cfg(feature = "remote")] + BlobFileInner::Remote(file) => file.read_up_to(len).await, + } + } + + /// Move the cursor to `new_cursor`. + pub async fn seek(&self, new_cursor: u64) -> lance_core::Result<()> { + match &self.inner { + BlobFileInner::Native(file) => file.seek(new_cursor).await, + #[cfg(feature = "remote")] + BlobFileInner::Remote(file) => file.seek(new_cursor).await, + } + } + + /// Current cursor position. + pub async fn tell(&self) -> lance_core::Result { + match &self.inner { + BlobFileInner::Native(file) => file.tell().await, + #[cfg(feature = "remote")] + BlobFileInner::Remote(file) => file.tell().await, + } + } + + /// Blob length in bytes. + pub fn size(&self) -> u64 { + match &self.inner { + BlobFileInner::Native(file) => file.size(), + #[cfg(feature = "remote")] + BlobFileInner::Remote(file) => file.size(), + } + } + + /// Physical byte offset in the data file. `None` on remote handles. The + /// Cloud byte-range route does not expose storage layout. + pub fn position(&self) -> Option { + match &self.inner { + BlobFileInner::Native(file) => Some(file.position()), + #[cfg(feature = "remote")] + BlobFileInner::Remote(_) => None, + } + } + + /// Path of the data file holding the blob. `None` on remote handles. The + /// Cloud byte-range route does not expose storage layout. + pub fn data_path(&self) -> Option<&Path> { + match &self.inner { + BlobFileInner::Native(file) => Some(file.data_path()), + #[cfg(feature = "remote")] + BlobFileInner::Remote(_) => None, + } + } + + /// Native storage layout. `None` on remote handles. The Cloud byte-range + /// route does not expose layout. + pub fn kind(&self) -> Option { + match &self.inner { + BlobFileInner::Native(file) => Some(file.kind()), + #[cfg(feature = "remote")] + BlobFileInner::Remote(_) => None, + } + } + + /// External URI for native handles. Remote handles do not expose storage URIs. + pub fn uri(&self) -> Option<&str> { + match &self.inner { + BlobFileInner::Native(file) => file.uri(), + #[cfg(feature = "remote")] + BlobFileInner::Remote(_) => None, + } + } +} /// One row-specific blob range read request. /// @@ -264,7 +457,10 @@ pub(crate) async fn take_blob_files_aligned( let handles = dataset.take_blobs(row_ids, column).await?; ensure_all_row_ids_resolved(column, row_ids.len(), handles.len())?; - Ok(handles) + Ok(handles + .into_iter() + .map(|handle| handle.map(Into::into)) + .collect()) } #[cfg(test)] diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index e9c200521..de3bbc94a 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright The LanceDB Authors -mod blobs; +pub mod blobs; pub mod insert; use self::insert::{RemoteWriteExec, WriteOp}; @@ -4300,32 +4300,9 @@ mod tests { "fetch_blobs", ); - let message = table - .fetch_blob_files("image", &[1]) - .await - .unwrap_err() - .to_string(); - assert!( - message.contains("fetch_blob_files is not supported on LanceDB Cloud"), - "got: {message}" - ); - assert!( - !message.contains("Use fetch_blobs"), - "old server must not be told to use fetch_blobs, got: {message}" - ); - } - - #[tokio::test] - async fn test_blob_files_point_at_fetch_blobs_on_a_blob_capable_server() { - let table = Table::new_with_handler_version( - "my_table", - semver::Version::new(0, 5, 0), - |_| -> http::Response { panic!("fetch_blob_files must not reach the server") }, - ); - assert_not_supported_error( table.fetch_blob_files("image", &[1]).await.unwrap_err(), - "Use fetch_blobs for full bytes", + "requires LanceDB Cloud server 0.5.0 or newer", ); } diff --git a/rust/lancedb/src/remote/table/blobs.rs b/rust/lancedb/src/remote/table/blobs.rs index 550f72a4b..bb8a5d103 100644 --- a/rust/lancedb/src/remote/table/blobs.rs +++ b/rust/lancedb/src/remote/table/blobs.rs @@ -1,21 +1,343 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright The LanceDB Authors -//! Cloud blob column listing and whole-byte fetch. +//! Cloud blob column listing, whole-byte fetch, and seekable HTTP byte-range handles. +use std::ops::Range; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use arrow_array::{Array, LargeBinaryArray}; use arrow_schema::DataType; -use futures::TryStreamExt; +use bytes::{Bytes, BytesMut}; +use futures::{StreamExt, TryStreamExt}; +use reqwest::{Response, StatusCode, header}; +use tokio::sync::Mutex; use crate::Error; use crate::blob::BlobFile; use crate::error::Result; -use crate::remote::client::HttpSend; +use crate::remote::client::{HttpSend, RequestResultExt, RestfulLanceDbClient}; use crate::table::BaseTable; -use super::RemoteTable; +use super::{FreshnessHeaders, RemoteTable}; + +#[derive(Debug, Clone, Copy)] +enum RangeRequestMode { + SizeProbe, + DataRead, +} + +#[async_trait::async_trait] +trait BlobRangeRequester: Send + Sync + std::fmt::Debug { + async fn request_range( + &self, + range_header: &str, + mode: RangeRequestMode, + ) -> Result<(String, Response)>; +} + +#[derive(Debug)] +struct TableBlobRangeRequester { + client: RestfulLanceDbClient, + path: String, + version: Option, + branch: Option, + freshness: FreshnessHeaders, +} + +#[async_trait::async_trait] +impl BlobRangeRequester for TableBlobRangeRequester { + async fn request_range( + &self, + range_header: &str, + mode: RangeRequestMode, + ) -> Result<(String, Response)> { + let mut request = self + .freshness + .apply(self.client.get(&self.path)) + .header(header::RANGE, range_header); + if let Some(version) = self.version { + request = request.query(&[("version", version)]); + } + if let Some(branch) = &self.branch { + request = request.query(&[("branch", branch)]); + } + let (request_id, response) = self.client.send_with_retry(request, None, true).await?; + // Preserve 416 size-probe responses so the caller can detect empty blobs. + if response.status() == StatusCode::RANGE_NOT_SATISFIABLE + && matches!(mode, RangeRequestMode::SizeProbe) + { + return Ok((request_id, response)); + } + let response = self.client.check_response(&request_id, response).await?; + Ok((request_id, response)) + } +} + +#[derive(Debug)] +struct SequentialResponse { + response: Response, + request_id: String, + buffered: Bytes, +} + +#[derive(Debug, Default)] +struct RemoteBlobState { + cursor: u64, + sequential_response: Option, +} + +/// Seekable Cloud blob handle over HTTP Range. +#[derive(Debug)] +pub(crate) struct RemoteBlobFile { + requester: Arc, + state: Mutex, + closed: AtomicBool, + size: u64, +} + +impl RemoteBlobFile { + fn new(requester: Arc, size: u64) -> Self { + Self { + requester, + state: Mutex::new(RemoteBlobState::default()), + closed: AtomicBool::new(false), + size, + } + } + + /// Close the handle without waiting for an in-flight read. + pub(crate) async fn close(&self) -> lance_core::Result<()> { + self.closed.store(true, Ordering::Release); + // Drop a retained response when the state lock is immediately available. + // A reader holding the lock drops it instead once it observes the flag. + if let Ok(mut state) = self.state.try_lock() { + state.sequential_response = None; + } + Ok(()) + } + + pub(crate) fn is_closed(&self) -> bool { + self.closed.load(Ordering::Acquire) + } + + fn ensure_open(&self) -> lance_core::Result<()> { + if self.closed.load(Ordering::Acquire) { + Err(lance_core::Error::invalid_input( + "blob file is already closed", + )) + } else { + Ok(()) + } + } + + pub(crate) async fn read_range(&self, range: Range) -> lance_core::Result { + self.ensure_open()?; + if range.start > range.end { + return Err(lance_core::Error::invalid_input(format!( + "blob range start {} exceeds end {}", + range.start, range.end + ))); + } + if range.end > self.size { + return Err(lance_core::Error::invalid_input(format!( + "blob range end {} exceeds blob size {}", + range.end, self.size + ))); + } + if range.is_empty() { + return Ok(Bytes::new()); + } + let range_header = format!("bytes={}-{}", range.start, range.end - 1); + let (request_id, response) = self + .requester + .request_range(&range_header, RangeRequestMode::DataRead) + .await + .map_err(remote_blob_error)?; + self.ensure_open()?; + validate_partial_response(&response, range.clone(), self.size)?; + let bytes = response + .bytes() + .await + .err_to_http(request_id) + .map_err(remote_blob_error)?; + self.ensure_open()?; + if bytes.len() as u64 != range.end - range.start { + return Err(remote_blob_error(format!( + "byte range returned {} bytes, expected {}", + bytes.len(), + range.end - range.start + ))); + } + Ok(bytes) + } + + /// Read ranges concurrently while preserving input order. + pub(crate) async fn read_ranges( + &self, + ranges: &[Range], + ) -> lance_core::Result> { + futures::stream::iter(ranges.iter().cloned().map(|range| self.read_range(range))) + .buffered(BLOB_REQUEST_CONCURRENCY) + .try_collect() + .await + } + + /// Read from the cursor to the end of the blob. + /// + /// Holds the state lock across cursor calculation and reading so a concurrent + /// seek cannot change the cursor between them. + pub(crate) async fn read(&self) -> lance_core::Result { + self.ensure_open()?; + let mut state = self.state.lock().await; + self.ensure_open()?; + let remaining = self.size.saturating_sub(state.cursor); + let remaining = usize::try_from(remaining).map_err(|_| { + lance_core::Error::invalid_input("remaining blob length exceeds addressable memory") + })?; + self.read_up_to_locked(&mut state, remaining).await + } + + pub(crate) async fn read_up_to(&self, len: usize) -> lance_core::Result { + self.ensure_open()?; + let mut state = self.state.lock().await; + self.ensure_open()?; + self.read_up_to_locked(&mut state, len).await + } + + /// Read up to `len` bytes using caller-validated, locked state. + async fn read_up_to_locked( + &self, + state: &mut RemoteBlobState, + len: usize, + ) -> lance_core::Result { + let target_len = self.size.saturating_sub(state.cursor).min(len as u64) as usize; + if target_len == 0 { + return Ok(Bytes::new()); + } + + // Remove the retained response from shared state before awaiting. Failed or + // cancelled reads leave the committed cursor unchanged and force the next + // read to open a fresh response. + let mut sequential_response = state.sequential_response.take(); + let mut cursor = state.cursor; + let mut output = BytesMut::with_capacity(target_len); + while output.len() < target_len { + if sequential_response.is_none() { + let range_header = format!("bytes={cursor}-"); + let (request_id, response) = self + .requester + .request_range(&range_header, RangeRequestMode::DataRead) + .await + .map_err(remote_blob_error)?; + self.ensure_open()?; + validate_partial_response(&response, cursor..self.size, self.size)?; + sequential_response = Some(SequentialResponse { + response, + request_id, + buffered: Bytes::new(), + }); + } + + let needed = target_len - output.len(); + let active = sequential_response.as_mut().unwrap(); + if !active.buffered.is_empty() { + let take = needed.min(active.buffered.len()); + output.extend_from_slice(&active.buffered.split_to(take)); + cursor += take as u64; + continue; + } + let chunk = active + .response + .chunk() + .await + .err_to_http(active.request_id.clone()) + .map_err(remote_blob_error)?; + self.ensure_open()?; + let chunk = chunk.ok_or_else(|| { + remote_blob_error("response ended before the requested blob range") + })?; + active.buffered = chunk; + } + self.ensure_open()?; + state.cursor = cursor; + if state.cursor < self.size { + state.sequential_response = sequential_response; + } + Ok(output.freeze()) + } + + pub(crate) async fn seek(&self, new_cursor: u64) -> lance_core::Result<()> { + self.ensure_open()?; + let mut state = self.state.lock().await; + self.ensure_open()?; + state.sequential_response = None; + state.cursor = new_cursor; + Ok(()) + } + + pub(crate) async fn tell(&self) -> lance_core::Result { + self.ensure_open()?; + let state = self.state.lock().await; + self.ensure_open()?; + Ok(state.cursor) + } + + pub(crate) fn size(&self) -> u64 { + self.size + } +} + +fn remote_blob_error(error: impl std::fmt::Display) -> lance_core::Error { + lance_core::Error::io(format!("remote blob read failed: {error}")) +} + +fn parse_content_range(value: &str) -> Option<(u64, u64, u64)> { + let value = value.strip_prefix("bytes ")?; + let (range, total) = value.split_once('/')?; + let (start, end) = range.split_once('-')?; + Some((start.parse().ok()?, end.parse().ok()?, total.parse().ok()?)) +} + +/// Parse the total from an unsatisfied-range header, `bytes */{total}`. +fn parse_unsatisfied_content_range(value: &str) -> Option { + value.strip_prefix("bytes */")?.parse().ok() +} + +/// Validate a partial response against the requested range and blob size. +/// +/// Reject `200 OK` because it may contain the entire object. +fn validate_partial_response( + response: &Response, + expected: Range, + size: u64, +) -> lance_core::Result<()> { + if response.status() != StatusCode::PARTIAL_CONTENT { + return Err(remote_blob_error(format!( + "expected HTTP 206 Partial Content, got {}", + response.status() + ))); + } + let content_range = response + .headers() + .get(header::CONTENT_RANGE) + .and_then(|value| value.to_str().ok()) + .and_then(parse_content_range) + .ok_or_else(|| remote_blob_error("response is missing a valid Content-Range header"))?; + if content_range != (expected.start, expected.end - 1, size) { + return Err(remote_blob_error(format!( + "expected Content-Range 'bytes {}-{}/{}', got 'bytes {}-{}/{}'", + expected.start, + expected.end - 1, + size, + content_range.0, + content_range.1, + content_range.2 + ))); + } + Ok(()) +} impl RemoteTable { /// Blob v2 columns are marked in field metadata, which `describe` returns. Reading @@ -30,8 +352,7 @@ impl RemoteTable { column: &str, row_ids: &[u64], ) -> Result { - // An empty selection already has its answer, so skip the round trip and the - // server requirement entirely. Local fetch_blobs returns early the same way. + // Empty requests do not require blob-route support. if row_ids.is_empty() { return Ok(LargeBinaryArray::from(Vec::>::new())); } @@ -109,19 +430,893 @@ impl RemoteTable { Ok(blobs) } + /// Open seekable handles for `row_ids`. + /// + /// Each non-null row is probed once to determine its size. pub(super) async fn fetch_blob_files_impl( &self, - _column: &str, - _row_ids: &[u64], + column: &str, + row_ids: &[u64], ) -> Result>> { - let message = if self.server_version.support_blobs() { - "fetch_blob_files is not supported on LanceDB Cloud yet. \ - Use fetch_blobs for full bytes" + // Empty requests do not require blob-route support. + if row_ids.is_empty() { + return Ok(Vec::new()); + } + if !self.server_version.support_blobs() { + return Err(Error::NotSupported { + message: "fetch_blob_files requires LanceDB Cloud server 0.5.0 or newer".into(), + }); + } + + let version = self.current_version().await; + let freshness = self.snapshot_freshness_headers(); + let encoded_column = urlencoding::encode(column); + let requesters = row_ids + .iter() + .map(|row_id| { + let path = format!( + "/v1/table/{}/blob/{encoded_column}/{row_id}/bytes", + self.identifier + ); + let requester: Arc = Arc::new(TableBlobRangeRequester { + client: self.client.clone(), + path, + version, + branch: self.branch.clone(), + freshness, + }); + requester + }) + .collect(); + probe_blob_files(requesters).await + } +} + +/// Probe blob sizes while preserving row order. +async fn probe_blob_files( + requesters: Vec>, +) -> Result>> { + // Collect before buffering to satisfy the async trait lifetime bounds. + let probe_futures: Vec<_> = requesters.into_iter().map(probe_blob_file).collect(); + futures::stream::iter(probe_futures) + .buffered(BLOB_REQUEST_CONCURRENCY) + .try_collect() + .await +} + +const BLOB_REQUEST_CONCURRENCY: usize = 8; + +/// Probe one blob's size. +/// +/// `204` represents null. `416` with `bytes */0` represents an empty blob. +async fn probe_blob_file(requester: Arc) -> Result> { + let (request_id, response) = requester + .request_range("bytes=0-0", RangeRequestMode::SizeProbe) + .await?; + match response.status() { + StatusCode::NO_CONTENT => { + response.bytes().await.err_to_http(request_id)?; + Ok(None) + } + StatusCode::RANGE_NOT_SATISFIABLE => { + let total = response + .headers() + .get(header::CONTENT_RANGE) + .and_then(|value| value.to_str().ok()) + .and_then(parse_unsatisfied_content_range); + match total { + Some(0) => {} + Some(total) => { + return Err(Error::Http { + source: format!( + "blob size probe returned HTTP 416 for a {total}-byte blob" + ) + .into(), + request_id, + status_code: Some(StatusCode::RANGE_NOT_SATISFIABLE), + }); + } + None => { + return Err(Error::Http { + source: "blob size probe returned an invalid Content-Range header".into(), + request_id, + status_code: Some(StatusCode::RANGE_NOT_SATISFIABLE), + }); + } + } + Ok(Some(RemoteBlobFile::new(requester, 0).into())) + } + StatusCode::PARTIAL_CONTENT => { + let size = response + .headers() + .get(header::CONTENT_RANGE) + .and_then(|value| value.to_str().ok()) + .and_then(parse_content_range) + .and_then(|(start, end, total)| { + (start == 0 && end == 0 && total > 0).then_some(total) + }) + .ok_or_else(|| Error::Http { + source: "blob size probe returned an invalid Content-Range header".into(), + request_id: request_id.clone(), + status_code: Some(StatusCode::PARTIAL_CONTENT), + })?; + let probe_body = response.bytes().await.err_to_http(request_id.clone())?; + if probe_body.len() != 1 { + return Err(Error::Http { + source: format!( + "blob size probe returned {} bytes, expected 1", + probe_body.len() + ) + .into(), + request_id, + status_code: Some(StatusCode::PARTIAL_CONTENT), + }); + } + Ok(Some(RemoteBlobFile::new(requester, size).into())) + } + status => Err(Error::Http { + source: format!("blob size probe expected HTTP 206 Partial Content, got {status}") + .into(), + request_id, + status_code: Some(status), + }), + } +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex as StdMutex}; + use std::time::Duration; + + use reqwest::Request; + use semver::Version; + + use super::*; + + const PAYLOAD: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz"; + + fn null_blob_response() -> http::Response> { + http::Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Vec::new()) + .unwrap() + } + + fn empty_blob_response() -> http::Response> { + http::Response::builder() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(header::CONTENT_RANGE, "bytes */0") + .body(Vec::new()) + .unwrap() + } + + fn range_response(request: &Request, payload: &[u8]) -> http::Response> { + let value = request + .headers() + .get(header::RANGE) + .unwrap() + .to_str() + .unwrap(); + let (start, end) = value + .strip_prefix("bytes=") + .unwrap() + .split_once('-') + .unwrap(); + let start = start.parse::().unwrap(); + let end = if end.is_empty() { + payload.len() - 1 } else { - "fetch_blob_files is not supported on LanceDB Cloud" + end.parse::().unwrap() }; - Err(Error::NotSupported { - message: message.into(), - }) + http::Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header( + header::CONTENT_RANGE, + format!("bytes {start}-{end}/{}", payload.len()), + ) + .body(payload[start..=end].to_vec()) + .unwrap() + } + + fn mock_remote_blob_table( + requests: Arc>>, + ) -> RemoteTable { + RemoteTable::new_mock( + "my_table".to_string(), + move |request| { + assert_eq!(request.method(), reqwest::Method::GET); + let path = request.url().path(); + assert!(path.starts_with("/v1/table/my_table/blob/image/")); + requests.lock().unwrap().push( + request + .headers() + .get(header::RANGE) + .unwrap() + .to_str() + .unwrap() + .to_string(), + ); + if path.contains("/20/bytes") { + return null_blob_response(); + } + if path.contains("/30/bytes") { + return empty_blob_response(); + } + range_response(&request, PAYLOAD) + }, + Some(Version::new(0, 5, 0)), + ) + } + + #[tokio::test] + async fn remote_blob_files_probe_sizes_and_preserve_nulls() { + let requests = Arc::new(StdMutex::new(Vec::new())); + let table = mock_remote_blob_table(requests.clone()); + + let mut files = table + .fetch_blob_files_impl("image", &[10, 20]) + .await + .unwrap(); + + assert_eq!(files.len(), 2); + assert!(files[1].is_none()); + let file = files[0].take().unwrap(); + assert_eq!(file.size(), PAYLOAD.len() as u64); + assert_eq!( + requests.lock().unwrap().as_slice(), + ["bytes=0-0", "bytes=0-0"] + ); + } + + #[tokio::test] + async fn remote_blob_file_reads_the_requested_range() { + let requests = Arc::new(StdMutex::new(Vec::new())); + let table = mock_remote_blob_table(requests.clone()); + let file = table + .fetch_blob_files_impl("image", &[10]) + .await + .unwrap() + .pop() + .flatten() + .unwrap(); + + assert_eq!(file.read_range(5..12).await.unwrap(), &PAYLOAD[5..12]); + assert!(requests.lock().unwrap().contains(&"bytes=5-11".to_string())); + } + + #[tokio::test] + async fn remote_blob_file_reuses_sequential_response_until_seek() { + let requests = Arc::new(StdMutex::new(Vec::new())); + let table = mock_remote_blob_table(requests.clone()); + let file = table + .fetch_blob_files_impl("image", &[10]) + .await + .unwrap() + .pop() + .flatten() + .unwrap(); + + assert_eq!(file.read_up_to(4).await.unwrap(), b"0123".as_slice()); + assert_eq!(file.read_up_to(3).await.unwrap(), b"456".as_slice()); + file.seek(20).await.unwrap(); + assert_eq!(file.read_up_to(4).await.unwrap(), &PAYLOAD[20..24]); + assert_eq!(file.tell().await.unwrap(), 24); + assert_eq!( + requests.lock().unwrap().as_slice(), + ["bytes=0-0", "bytes=0-", "bytes=20-"] + ); + } + + #[tokio::test] + async fn remote_blob_files_return_a_handle_for_an_empty_blob() { + let requests = Arc::new(StdMutex::new(Vec::new())); + let table = mock_remote_blob_table(requests.clone()); + + let mut files = table + .fetch_blob_files_impl("image", &[10, 20, 30]) + .await + .unwrap(); + + assert_eq!(files.len(), 3); + assert!(files[1].is_none()); + let empty = files[2].take().unwrap(); + assert_eq!(empty.size(), 0); + assert!(empty.read_range(0..0).await.unwrap().is_empty()); + assert!(empty.read().await.unwrap().is_empty()); + let nonempty = files[0].take().unwrap(); + assert_eq!(nonempty.size(), PAYLOAD.len() as u64); + } + + #[tokio::test] + async fn remote_blob_files_reject_unsatisfied_probe_for_nonempty_blob() { + let table = RemoteTable::new_mock( + "my_table".to_string(), + |_| { + http::Response::builder() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(header::CONTENT_RANGE, "bytes */36") + .body(Vec::new()) + .unwrap() + }, + Some(Version::new(0, 5, 0)), + ); + + let error = table + .fetch_blob_files_impl("image", &[10]) + .await + .unwrap_err(); + assert!( + error + .to_string() + .contains("blob size probe returned HTTP 416 for a 36-byte blob"), + "got: {error}" + ); + } + + #[tokio::test] + async fn remote_blob_files_reject_a_self_contradictory_probe_response() { + // A zero-length blob must use `416 Content-Range: bytes */0`. + let table = RemoteTable::new_mock( + "my_table".to_string(), + |_| { + http::Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header(header::CONTENT_RANGE, "bytes 0-0/0") + .body(vec![0u8]) + .unwrap() + }, + Some(Version::new(0, 5, 0)), + ); + + let error = table + .fetch_blob_files_impl("image", &[10]) + .await + .unwrap_err(); + assert!( + error + .to_string() + .contains("blob size probe returned an invalid Content-Range header"), + "got: {error}" + ); + } + + #[tokio::test] + async fn remote_blob_files_reject_unsupported_server_version() { + let table = RemoteTable::new_mock( + "my_table".to_string(), + |_| -> http::Response { + panic!("old servers must be rejected before a range request") + }, + Some(Version::new(0, 4, 9)), + ); + let error = table + .fetch_blob_files_impl("image", &[10]) + .await + .unwrap_err(); + assert!(matches!(error, Error::NotSupported { .. })); + assert!(error.to_string().contains("0.5.0")); + } + + #[tokio::test] + async fn remote_blob_file_rejects_mismatched_content_range() { + let requests = Arc::new(StdMutex::new(Vec::new())); + let table = RemoteTable::new_mock( + "my_table".to_string(), + { + let requests = requests.clone(); + move |request| { + let range = request + .headers() + .get(header::RANGE) + .unwrap() + .to_str() + .unwrap() + .to_string(); + requests.lock().unwrap().push(range.clone()); + if range == "bytes=0-0" { + return range_response(&request, PAYLOAD); + } + http::Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header( + header::CONTENT_RANGE, + format!("bytes 6-12/{}", PAYLOAD.len()), + ) + .body(PAYLOAD[6..=12].to_vec()) + .unwrap() + } + }, + Some(Version::new(0, 5, 0)), + ); + let file = table + .fetch_blob_files_impl("image", &[10]) + .await + .unwrap() + .pop() + .flatten() + .unwrap(); + + let error = file.read_range(5..12).await.unwrap_err(); + assert!( + error.to_string().contains("expected Content-Range"), + "got: {error}" + ); + } + + #[tokio::test] + async fn remote_blob_file_rejects_short_response_body() { + let table = RemoteTable::new_mock( + "my_table".to_string(), + move |request| { + let range = request + .headers() + .get(header::RANGE) + .unwrap() + .to_str() + .unwrap(); + if range == "bytes=0-0" { + return range_response(&request, PAYLOAD); + } + http::Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header( + header::CONTENT_RANGE, + format!("bytes 5-11/{}", PAYLOAD.len()), + ) + .body(PAYLOAD[5..=7].to_vec()) + .unwrap() + }, + Some(Version::new(0, 5, 0)), + ); + let file = table + .fetch_blob_files_impl("image", &[10]) + .await + .unwrap() + .pop() + .flatten() + .unwrap(); + + let error = file.read_range(5..12).await.unwrap_err(); + assert!( + error.to_string().contains("returned 3 bytes, expected 7"), + "got: {error}" + ); + } + + #[tokio::test] + async fn remote_blob_file_failed_read_preserves_cursor_and_retries_fresh() { + let sequential_requests = Arc::new(AtomicUsize::new(0)); + let table = RemoteTable::new_mock( + "my_table".to_string(), + { + let sequential_requests = sequential_requests.clone(); + move |request| { + let range = request + .headers() + .get(header::RANGE) + .unwrap() + .to_str() + .unwrap() + .to_string(); + if range == "bytes=0-0" { + return range_response(&request, PAYLOAD); + } + let attempt = sequential_requests.fetch_add(1, Ordering::SeqCst); + if attempt == 0 { + // End the response five bytes early to simulate a truncated + // sequential read. + return http::Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header( + header::CONTENT_RANGE, + format!("bytes 0-{}/{}", PAYLOAD.len() - 1, PAYLOAD.len()), + ) + .body(PAYLOAD[..5].to_vec()) + .unwrap(); + } + range_response(&request, PAYLOAD) + } + }, + Some(Version::new(0, 5, 0)), + ); + let file = table + .fetch_blob_files_impl("image", &[10]) + .await + .unwrap() + .pop() + .flatten() + .unwrap(); + + let error = file.read_up_to(10).await.unwrap_err(); + assert!( + error + .to_string() + .contains("response ended before the requested blob range"), + "got: {error}" + ); + assert_eq!(file.tell().await.unwrap(), 0); + + // Retry from the last committed cursor with a fresh request. + let retried = file.read_up_to(4).await.unwrap(); + assert_eq!(retried, b"0123".as_slice()); + assert_eq!(sequential_requests.load(Ordering::SeqCst), 2); + } + + #[tokio::test] + async fn remote_blob_file_empty_range_sends_no_request() { + let requests = Arc::new(StdMutex::new(Vec::new())); + let table = mock_remote_blob_table(requests.clone()); + let file = table + .fetch_blob_files_impl("image", &[10]) + .await + .unwrap() + .pop() + .flatten() + .unwrap(); + + assert!(file.read_range(3..3).await.unwrap().is_empty()); + assert_eq!(requests.lock().unwrap().as_slice(), ["bytes=0-0"]); + } + + #[derive(Debug)] + struct CountingProbeRequester { + index: usize, + in_flight: Arc, + max_in_flight: Arc, + } + + #[async_trait::async_trait] + impl BlobRangeRequester for CountingProbeRequester { + async fn request_range( + &self, + range_header: &str, + _mode: RangeRequestMode, + ) -> Result<(String, Response)> { + assert_eq!(range_header, "bytes=0-0"); + let now = self.in_flight.fetch_add(1, Ordering::SeqCst) + 1; + self.max_in_flight.fetch_max(now, Ordering::SeqCst); + // Earlier probes sleep longer, so later probes finish first and the + // ordered collection has to do real reordering work. + tokio::time::sleep(Duration::from_millis(20 - self.index as u64)).await; + self.in_flight.fetch_sub(1, Ordering::SeqCst); + let response = http::Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header( + header::CONTENT_RANGE, + format!("bytes 0-0/{}", 100 + self.index), + ) + .body(vec![0u8]) + .unwrap(); + Ok((format!("probe-{}", self.index), Response::from(response))) + } + } + + #[tokio::test] + async fn remote_blob_file_probes_are_bounded_and_preserve_order() { + let in_flight = Arc::new(AtomicUsize::new(0)); + let max_in_flight = Arc::new(AtomicUsize::new(0)); + let probes = (0..16) + .map(|index| { + let requester: Arc = Arc::new(CountingProbeRequester { + index, + in_flight: in_flight.clone(), + max_in_flight: max_in_flight.clone(), + }); + requester + }) + .collect(); + + let files = probe_blob_files(probes).await.unwrap(); + + let sizes: Vec = files.into_iter().map(|file| file.unwrap().size()).collect(); + let expected: Vec = (0..16).map(|index| 100 + index as u64).collect(); + assert_eq!(sizes, expected); + let max = max_in_flight.load(Ordering::SeqCst); + assert!(max > 1, "probes never overlapped"); + assert!( + max <= BLOB_REQUEST_CONCURRENCY, + "{max} probes in flight exceeds the bound" + ); + } + + #[tokio::test] + async fn remote_blob_file_metadata_reports_none() { + let requests = Arc::new(StdMutex::new(Vec::new())); + let table = mock_remote_blob_table(requests.clone()); + + let mut files = table.fetch_blob_files_impl("image", &[10]).await.unwrap(); + let file = files.remove(0).unwrap(); + + assert_eq!(file.size(), PAYLOAD.len() as u64); + assert_eq!(file.position(), None); + assert_eq!(file.kind(), None); + assert_eq!(file.data_path(), None); + assert_eq!(file.uri(), None); + } + + #[tokio::test] + async fn closed_remote_blob_file_rejects_every_operation_without_requests() { + let requests = Arc::new(StdMutex::new(Vec::new())); + let table = mock_remote_blob_table(requests.clone()); + + let mut files = table.fetch_blob_files_impl("image", &[10]).await.unwrap(); + let file = files.remove(0).unwrap(); + let probe_requests = requests.lock().unwrap().len(); + + file.close().await.unwrap(); + + assert!(file.is_closed().await); + for error in [ + file.read().await.unwrap_err(), + file.read_range(0..1).await.unwrap_err(), + file.read_ranges(&[0..1, 1..2]).await.unwrap_err(), + file.read_up_to(1).await.unwrap_err(), + file.seek(0).await.unwrap_err(), + file.tell().await.unwrap_err(), + ] { + assert!(error.to_string().contains("already closed"), "got: {error}"); + } + assert_eq!(requests.lock().unwrap().len(), probe_requests); + } + + #[tokio::test] + async fn out_of_range_read_fails_without_a_request() { + let requests = Arc::new(StdMutex::new(Vec::new())); + let table = mock_remote_blob_table(requests.clone()); + + let mut files = table.fetch_blob_files_impl("image", &[10]).await.unwrap(); + let file = files.remove(0).unwrap(); + let probe_requests = requests.lock().unwrap().len(); + + let past_end = file.read_range(0..file.size() + 1).await.unwrap_err(); + assert!( + past_end.to_string().contains("exceeds blob size"), + "got: {past_end}" + ); + let inverted = file + .read_range(Range { start: 3, end: 1 }) + .await + .unwrap_err(); + assert!( + inverted.to_string().contains("exceeds end"), + "got: {inverted}" + ); + assert_eq!(requests.lock().unwrap().len(), probe_requests); + } + + #[tokio::test] + async fn data_read_rejects_416_response() { + let table = RemoteTable::new_mock( + "my_table".to_string(), + |request| { + let range = request + .headers() + .get(header::RANGE) + .unwrap() + .to_str() + .unwrap() + .to_string(); + if range == "bytes=0-0" { + return http::Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header( + header::CONTENT_RANGE, + format!("bytes 0-0/{}", PAYLOAD.len()), + ) + .body(vec![PAYLOAD[0]]) + .unwrap(); + } + http::Response::builder() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(header::CONTENT_RANGE, "bytes */0") + .body(b"stale range".to_vec()) + .unwrap() + }, + Some(Version::new(0, 5, 0)), + ); + + let mut files = table.fetch_blob_files_impl("image", &[10]).await.unwrap(); + let file = files.remove(0).unwrap(); + + let error = file.read_range(1..3).await.unwrap_err(); + assert!(error.to_string().contains("416"), "got: {error}"); + } + + #[tokio::test] + async fn empty_row_id_lists_bypass_server_version_gate() { + let table = RemoteTable::new_mock( + "my_table".to_string(), + |_| -> http::Response { panic!("an empty selection sends no request") }, + Some(Version::new(0, 4, 9)), + ); + + let files = table.fetch_blob_files_impl("image", &[]).await.unwrap(); + assert!(files.is_empty()); + + let blobs = table.fetch_blobs_impl("image", &[]).await.unwrap(); + assert_eq!(blobs.len(), 0); + } + + #[derive(Debug)] + struct CountingRangeRequester { + in_flight: Arc, + max_in_flight: Arc, + } + + #[async_trait::async_trait] + impl BlobRangeRequester for CountingRangeRequester { + async fn request_range( + &self, + range_header: &str, + _mode: RangeRequestMode, + ) -> Result<(String, Response)> { + let now = self.in_flight.fetch_add(1, Ordering::SeqCst) + 1; + self.max_in_flight.fetch_max(now, Ordering::SeqCst); + tokio::time::sleep(Duration::from_millis(5)).await; + self.in_flight.fetch_sub(1, Ordering::SeqCst); + let (start, end) = range_header + .strip_prefix("bytes=") + .unwrap() + .split_once('-') + .unwrap(); + let start = start.parse::().unwrap(); + let end = end.parse::().unwrap(); + let response = http::Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header( + header::CONTENT_RANGE, + format!("bytes {start}-{end}/{}", PAYLOAD.len()), + ) + .body(PAYLOAD[start..=end].to_vec()) + .unwrap(); + Ok(("range".to_string(), Response::from(response))) + } + } + + #[tokio::test] + async fn read_ranges_run_bounded_and_preserve_order() { + let in_flight = Arc::new(AtomicUsize::new(0)); + let max_in_flight = Arc::new(AtomicUsize::new(0)); + let requester: Arc = Arc::new(CountingRangeRequester { + in_flight, + max_in_flight: max_in_flight.clone(), + }); + let file = RemoteBlobFile::new(requester, PAYLOAD.len() as u64); + + let ranges: Vec<_> = (0..16u64).map(|start| start..start + 2).collect(); + let output = file.read_ranges(&ranges).await.unwrap(); + + for (range, bytes) in ranges.iter().zip(&output) { + assert_eq!( + bytes.as_ref(), + &PAYLOAD[range.start as usize..range.end as usize] + ); + } + let max = max_in_flight.load(Ordering::SeqCst); + assert!(max > 1, "range reads never overlapped"); + assert!( + max <= BLOB_REQUEST_CONCURRENCY, + "{max} range reads in flight exceeds the bound" + ); + } + + #[tokio::test] + async fn read_ranges_reject_out_of_bounds_range() { + let requests = Arc::new(StdMutex::new(Vec::new())); + let table = mock_remote_blob_table(requests.clone()); + let file = table + .fetch_blob_files_impl("image", &[10]) + .await + .unwrap() + .pop() + .flatten() + .unwrap(); + + let oob = file + .read_ranges(&[0..2, 0..PAYLOAD.len() as u64 + 1]) + .await + .unwrap_err(); + assert!(oob.to_string().contains("exceeds blob size"), "got: {oob}"); + } + + #[tokio::test] + async fn range_read_rejects_200_response() { + let table = RemoteTable::new_mock( + "my_table".to_string(), + |request| { + let range = request + .headers() + .get(header::RANGE) + .unwrap() + .to_str() + .unwrap() + .to_string(); + if range == "bytes=0-0" { + return http::Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header( + header::CONTENT_RANGE, + format!("bytes 0-0/{}", PAYLOAD.len()), + ) + .body(vec![PAYLOAD[0]]) + .unwrap(); + } + http::Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_LENGTH, PAYLOAD.len().to_string()) + .body(PAYLOAD.to_vec()) + .unwrap() + }, + Some(Version::new(0, 5, 0)), + ); + + let mut files = table.fetch_blob_files_impl("image", &[10]).await.unwrap(); + let file = files.remove(0).unwrap(); + let error = file.read_range(1..3).await.unwrap_err(); + assert!(error.to_string().contains("206"), "got: {error}"); + } + + #[derive(Debug)] + struct BlockingRangeRequester { + release: Arc, + started: Arc, + } + + #[async_trait::async_trait] + impl BlobRangeRequester for BlockingRangeRequester { + async fn request_range( + &self, + range_header: &str, + _mode: RangeRequestMode, + ) -> Result<(String, Response)> { + if range_header.ends_with('-') { + self.started.notify_one(); + self.release.wait().await; + } + let response = http::Response::builder() + .status(StatusCode::PARTIAL_CONTENT) + .header( + header::CONTENT_RANGE, + format!("bytes 0-{}/{}", PAYLOAD.len() - 1, PAYLOAD.len()), + ) + .body(PAYLOAD.to_vec()) + .unwrap(); + Ok(("hung".to_string(), Response::from(response))) + } + } + + #[tokio::test] + async fn close_returns_while_a_sequential_read_is_in_flight() { + let release = Arc::new(tokio::sync::Barrier::new(2)); + let started = Arc::new(tokio::sync::Notify::new()); + let requester: Arc = Arc::new(BlockingRangeRequester { + release: release.clone(), + started: started.clone(), + }); + let file = Arc::new(RemoteBlobFile::new(requester, PAYLOAD.len() as u64)); + + let reader = { + let file = file.clone(); + tokio::spawn(async move { file.read_up_to(4).await }) + }; + started.notified().await; + + tokio::time::timeout(Duration::from_millis(50), file.close()) + .await + .expect("close waited on the hung read") + .unwrap(); + assert!(file.is_closed()); + + release.wait().await; + let error = reader.await.unwrap().unwrap_err(); + assert!(error.to_string().contains("already closed"), "got: {error}"); + assert!( + file.read_range(0..1) + .await + .unwrap_err() + .to_string() + .contains("already closed") + ); } } diff --git a/rust/lancedb/src/table.rs b/rust/lancedb/src/table.rs index 1c9d68f4c..6f8ecaf14 100644 --- a/rust/lancedb/src/table.rs +++ b/rust/lancedb/src/table.rs @@ -3,6 +3,7 @@ //! LanceDB Table APIs +use crate::blob::BlobFile; use arrow_array::{LargeBinaryArray, RecordBatch, RecordBatchReader}; use arrow_schema::{Schema, SchemaRef}; use async_trait::async_trait; @@ -12,7 +13,6 @@ use datafusion_physical_plan::ExecutionPlan; use datafusion_physical_plan::display::DisplayableExecutionPlan; use futures::StreamExt; use futures::stream::FuturesUnordered; -use lance::dataset::BlobFile; pub use lance::dataset::ColumnAlteration; pub use lance::dataset::NewColumnTransform; pub use lance::dataset::ReadParams; From e6ae93f52a7e18e777bdc07337ceba1c268df24c Mon Sep 17 00:00:00 2001 From: Adityaj0 <93090622+Adityaj0@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:54:41 -0700 Subject: [PATCH 29/29] fix: hybrid search minimum_nprobes(0) silently no-ops instead of raising (#3770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `LanceHybridQueryBuilder._create_query_builders()` checked `self._minimum_nprobes` for truthiness instead of `is not None` — the very next line correctly checks `is not None` for `self._maximum_nprobes`. Since `0` is falsy in Python, `.minimum_nprobes(0)` on a hybrid query silently dropped the value instead of forwarding it to the vector sub-query, where it would raise the same `ValueError` a plain vector query raises for the same input (`minimum_nprobes must be greater than 0`, validated in `rust/lancedb/src/query.rs` and covered for the plain-query path by `test_invalid_nprobes_sync`). Fixes #3766 ## Change One-line fix: `if self._minimum_nprobes:` → `if self._minimum_nprobes is not None:`, matching the existing `maximum_nprobes` check right below it. ## Test plan - [x] New regression test `test_hybrid_query_minimum_nprobes_zero_raises` in `python/python/tests/test_hybrid_query.py` - [x] `uv run --extra tests pytest python/tests/test_hybrid_query.py -vv` — 13 passed - [x] `uv run --extra dev ruff format` / `ruff check` — clean Co-authored-by: Claude Sonnet 5 --- python/python/lancedb/query.py | 2 +- python/python/tests/test_hybrid_query.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/python/python/lancedb/query.py b/python/python/lancedb/query.py index 60c5d0c68..095a7b5ff 100644 --- a/python/python/lancedb/query.py +++ b/python/python/lancedb/query.py @@ -2697,7 +2697,7 @@ class LanceHybridQueryBuilder(LanceQueryBuilder): self._fts_query.phrase_query(True) if self._distance_type: self._vector_query.metric(self._distance_type) - if self._minimum_nprobes: + if self._minimum_nprobes is not None: self._vector_query.minimum_nprobes(self._minimum_nprobes) if self._maximum_nprobes is not None: self._vector_query.maximum_nprobes(self._maximum_nprobes) diff --git a/python/python/tests/test_hybrid_query.py b/python/python/tests/test_hybrid_query.py index aa3e3f5fd..65a7890bf 100644 --- a/python/python/tests/test_hybrid_query.py +++ b/python/python/tests/test_hybrid_query.py @@ -123,6 +123,19 @@ async def test_async_hybrid_query_default_limit(table: AsyncTable): assert texts.count("a") == 1 +def test_hybrid_query_minimum_nprobes_zero_raises(sync_table: Table): + # minimum_nprobes(0) must raise the same validation error a plain vector + # query raises, not silently no-op because 0 is falsy. + with pytest.raises(ValueError, match="minimum_nprobes must be greater than 0"): + ( + sync_table.search(query_type="hybrid") + .vector([0.0, 0.4]) + .text("dog") + .minimum_nprobes(0) + .to_arrow() + ) + + def test_hybrid_query_distance_range(sync_table: Table): reranker = RRFReranker(return_score="all") result = (