diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfbf6ca7..771687be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,8 +35,8 @@ jobs: with: version: "0.12.1" enable-cache: true - - run: uv python install 3.11 - - run: uv sync --locked --extra dev --python 3.11 + - run: uv python install 3.12 + - run: uv sync --locked --extra dev --python 3.12 - run: make ci-lint test-cpu: @@ -46,9 +46,6 @@ jobs: fail-fast: false matrix: include: - - name: linux-x64 - runner: ubuntu-24.04 - python: "3.11" - name: linux-x64 runner: ubuntu-24.04 python: "3.12" @@ -60,7 +57,7 @@ jobs: python: "3.14" - name: linux-arm64 runner: ubuntu-24.04-arm - python: "3.11" + python: "3.12" runs-on: ${{ matrix.runner }} env: CUPHOTON_XREP_TORCH_DEVICE: cpu @@ -77,155 +74,22 @@ jobs: - run: uv python install "${PYTHON_VERSION}" - run: >- uv sync --locked --python "${PYTHON_VERSION}" - --extra dev --extra torch --extra viz + --extra dev --extra torch --extra viz --extra photometry - run: make ci-test-cpu - name: Run synthetic CPU quickstarts - if: matrix.name == 'linux-x64' && matrix.python == '3.11' + if: matrix.name == 'linux-x64' && matrix.python == '3.12' run: >- - uv run --locked --extra dev --extra torch --extra viz + uv run --locked --extra dev --extra torch --extra viz --extra photometry python examples/run_quickstarts.py --profile cpu --output-dir "${RUNNER_TEMP}/cuphoton-quickstart" package: - timeout-minutes: 30 - name: build / metadata / install smoke - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - with: - persist-credentials: false - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 - with: - version: "0.12.1" - enable-cache: true - - run: uv python install 3.11 - - run: uv sync --locked --extra dev --python 3.11 - - run: make package-check - - name: Verify the distribution contract - run: | - python - <<'PY' - from pathlib import Path - import re - import tarfile - import zipfile - - def is_shared_library(name): - filename = Path(name).name.lower() - return filename.endswith((".dll", ".dylib", ".pyd", ".so")) or ( - re.search(r"\.so(?:\.[0-9]+)+\Z", filename) is not None - ) - - - dist = Path("dist") - wheels = list(dist.glob("*.whl")) - sdists = list(dist.glob("*.tar.gz")) - assert len(wheels) == 1, wheels - assert len(sdists) == 1, sdists - - wheel = wheels[0] - assert wheel.name.endswith("-py3-none-any.whl"), wheel.name - with zipfile.ZipFile(wheel) as archive: - names = archive.namelist() - shared_libraries = [ - name - for name in names - if is_shared_library(name) - ] - assert not shared_libraries, shared_libraries - metadata_name = next( - name for name in names if name.endswith(".dist-info/WHEEL") - ) - metadata = archive.read(metadata_name).decode("utf-8") - assert "Root-Is-Purelib: true" in metadata - assert "Tag: py3-none-any" in metadata - - required_native_sources = { - "build.sh", - "io.cpp", - "memory_manager.cpp", - "nvcomp_batch_ext.cpp", - "nvcomp_batch_ext.h", - } - with tarfile.open(sdists[0], "r:gz") as archive: - names = archive.getnames() - source_names = { - Path(name).name - for name in names - if "/src/cuphoton/xdr/src/" in name - } - shared_libraries = [ - name - for name in names - if is_shared_library(name) - ] - assert required_native_sources <= source_names, source_names - assert not shared_libraries, shared_libraries - PY - - run: uv venv --seed --python 3.11 .wheel-venv - - run: .wheel-venv/bin/python -m pip install dist/*.whl - - run: .wheel-venv/bin/python -m pip check - - run: uv venv --seed --python 3.11 .sdist-venv - - run: .sdist-venv/bin/python -m pip install dist/*.tar.gz - - run: .sdist-venv/bin/python -m pip check - - name: Smoke-test installed distributions and CLIs - run: | - for environment in .wheel-venv .sdist-venv; do - "${environment}"/bin/python -c \ - "import cuphoton; import cuphoton.xdr; import cuphoton.xfit; import cuphoton.xpois; import cuphoton.xscan; import cuphoton.xrep; import cuphoton.xray; print(cuphoton.__version__)" - "${environment}"/bin/cuphoton --help >/dev/null - "${environment}"/bin/cuphoton --version - for group in xdr xfit xpois xscan xrep xray; do - "${environment}"/bin/cuphoton "${group}" --help >/dev/null - done - done - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: cuphoton-distributions - path: dist/* - if-no-files-found: error - retention-days: 7 - - package-compatibility: - timeout-minutes: 20 - name: install smoke / py${{ matrix.python }} - needs: package - strategy: - fail-fast: false - matrix: - python: ["3.12", "3.13", "3.14"] - runs-on: ubuntu-24.04 - env: - PYTHON_VERSION: ${{ matrix.python }} - steps: - - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 - with: - version: "0.12.1" - - run: uv python install "${PYTHON_VERSION}" - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cuphoton-distributions - path: dist - - run: uv venv --seed --python "${PYTHON_VERSION}" .wheel-venv - - run: .wheel-venv/bin/python -m pip install dist/*.whl - - run: .wheel-venv/bin/python -m pip check - - run: uv venv --seed --python "${PYTHON_VERSION}" .sdist-venv - - run: .sdist-venv/bin/python -m pip install dist/*.tar.gz - - run: .sdist-venv/bin/python -m pip check - - name: Smoke-test installed distributions and CLIs - run: | - for environment in .wheel-venv .sdist-venv; do - "${environment}"/bin/python -c \ - "import cuphoton; import cuphoton.xdr; import cuphoton.xfit; import cuphoton.xpois; import cuphoton.xscan; import cuphoton.xrep; import cuphoton.xray; print(cuphoton.__version__)" - "${environment}"/bin/cuphoton --help >/dev/null - "${environment}"/bin/cuphoton --version - for group in xdr xfit xpois xscan xrep xray; do - "${environment}"/bin/cuphoton "${group}" --help >/dev/null - done - done + name: native distributions + uses: ./.github/workflows/wheels.yml ci-required: name: ci-required - needs: [lint, test-cpu, package, package-compatibility] + needs: [lint, test-cpu, package] if: ${{ always() }} runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -235,12 +99,7 @@ jobs: LINT_RESULT: ${{ needs.lint.result }} CPU_RESULT: ${{ needs.test-cpu.result }} PACKAGE_RESULT: ${{ needs.package.result }} - COMPATIBILITY_RESULT: ${{ needs.package-compatibility.result }} run: | - for result in "${LINT_RESULT}" "${CPU_RESULT}" "${PACKAGE_RESULT}" "${COMPATIBILITY_RESULT}"; do - if [[ "${result}" != success ]]; then - printf 'Required CI jobs did not succeed: lint=%s, tests=%s, package=%s, compatibility=%s\n' \ - "${LINT_RESULT}" "${CPU_RESULT}" "${PACKAGE_RESULT}" "${COMPATIBILITY_RESULT}" - exit 1 - fi + for result in "${LINT_RESULT}" "${CPU_RESULT}" "${PACKAGE_RESULT}"; do + test "${result}" = success || exit 1 done diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..e3b8f82b --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,152 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +name: publish + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + version: + description: Existing release tag version, such as 0.1.3rc0 + required: true + type: string + target: + description: Protected publishing environment + required: true + type: choice + options: [testpypi, pypi] + default: testpypi + run-id: + description: Reuse this release build run instead of rebuilding (optional) + required: false + type: string + +permissions: + contents: read + +concurrency: + group: publish-${{ inputs.target || 'testpypi' }}-${{ github.event_name == 'push' && github.ref_name || format('v{0}', inputs.version) }} + cancel-in-progress: false + +jobs: + prepare: + if: >- + (github.event_name == 'push' && !github.event.deleted) || + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + version: ${{ steps.release.outputs.version }} + sha: ${{ steps.release.outputs.sha }} + tag: ${{ steps.release.outputs.tag }} + target: ${{ inputs.target || 'testpypi' }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + fetch-depth: 0 + - name: Resolve the release tag to a reviewed source commit + id: release + env: + RELEASE_TAG: ${{ github.event_name == 'push' && github.ref_name || format('v{0}', inputs.version) }} + EXPECTED_SHA: ${{ github.event_name == 'push' && github.sha || '' }} + BUILD_RUN_ID: ${{ inputs.run-id }} + run: | + if test -n "$BUILD_RUN_ID"; then + [[ "$BUILD_RUN_ID" =~ ^[1-9][0-9]*$ ]] + fi + python scripts/wheels/release.py resolve "$RELEASE_TAG" --expected-sha "$EXPECTED_SHA" + + build: + name: release wheels + needs: prepare + if: inputs.run-id == '' + uses: ./.github/workflows/wheels.yml + with: + source-ref: ${{ needs.prepare.outputs.sha }} + release-version: ${{ needs.prepare.outputs.version }} + + stage: + needs: [prepare, build] + if: >- + !cancelled() && needs.prepare.result == 'success' && + (needs.build.result == 'success' || + (needs.build.result == 'skipped' && inputs.run-id != '')) + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + actions: read + contents: read + outputs: + pending: ${{ steps.upload.outputs.pending }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + fetch-depth: 0 + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + with: + version: "0.12.1" + enable-cache: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cuphoton-distributions + path: dist + run-id: ${{ inputs.run-id || github.run_id }} + github-token: ${{ github.token }} + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cuphoton-build-provenance + path: provenance + run-id: ${{ inputs.run-id || github.run_id }} + github-token: ${{ github.token }} + - name: Verify the build, tag, complete matrix, and unchanged bytes + env: + GH_TOKEN: ${{ github.token }} + BUILD_RUN_ID: ${{ inputs.run-id || github.run_id }} + RELEASE_TAG: ${{ needs.prepare.outputs.tag }} + RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + SOURCE_SHA: ${{ needs.prepare.outputs.sha }} + run: | + python scripts/wheels/release.py resolve "$RELEASE_TAG" --expected-sha "$SOURCE_SHA" + python scripts/wheels/check_distributions.py dist --version "$RELEASE_VERSION" + python scripts/wheels/release.py verify dist provenance/provenance.json \ + --tag "$RELEASE_TAG" --sha "$SOURCE_SHA" --run-id "$BUILD_RUN_ID" + uvx --isolated --from twine==6.2.0 twine check --strict dist/* + cat provenance/provenance.json >> "$GITHUB_STEP_SUMMARY" + - name: Exclude already published files only when their hashes match + id: upload + env: + RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + TARGET: ${{ needs.prepare.outputs.target }} + run: | + python scripts/wheels/release.py pending dist --version "$RELEASE_VERSION" --target "$TARGET" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: steps.upload.outputs.pending != '0' + with: + name: publish-distributions + path: dist/* + if-no-files-found: error + + publish: + needs: [prepare, stage] + if: >- + !cancelled() && needs.prepare.result == 'success' && + needs.stage.result == 'success' && needs.stage.outputs.pending != '0' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + environment: + name: ${{ needs.prepare.outputs.target }} + permissions: + id-token: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: publish-distributions + path: dist + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 + with: + repository-url: ${{ needs.prepare.outputs.target == 'testpypi' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 00000000..c3c50504 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,269 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +name: native wheels + +on: + workflow_call: + inputs: + source-ref: + description: Immutable source commit to build + required: false + type: string + default: "" + release-version: + description: Version from the validated release tag + required: false + type: string + default: "" + workflow_dispatch: + +permissions: + contents: read + +jobs: + sdist: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + ref: ${{ inputs.source-ref || github.sha }} + fetch-depth: 0 + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + with: + version: "0.12.1" + enable-cache: false + - name: Build the versioned source archive + env: + RELEASE_VERSION: ${{ inputs.release-version }} + run: | + if test -n "$RELEASE_VERSION"; then + export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUPHOTON="$RELEASE_VERSION" + fi + CUPHOTON_XDR_BUILD_EXT=0 uv build --sdist + - name: Check the default source installation + run: | + uv venv --python 3.12 .sdist-venv + CUPHOTON_XDR_BUILD_EXT=0 uv pip install --python .sdist-venv/bin/python dist/*.tar.gz + uv pip check --python .sdist-venv/bin/python + .sdist-venv/bin/python -I -c 'import cuphoton; import cuphoton.xdr; print(cuphoton.__version__)' + .sdist-venv/bin/cuphoton xdr --help + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cuphoton-sdist + path: dist/*.tar.gz + if-no-files-found: error + + wheels: + needs: sdist + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-24.04 + - arch: aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + steps: + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + with: + version: "0.12.1" + enable-cache: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cuphoton-sdist + path: dist + - name: Build and test all three ABIs from the source archive + run: >- + uv tool run --from cibuildwheel==4.2.1 cibuildwheel + --platform linux --output-dir wheelhouse dist/*.tar.gz + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cuphoton-wheels-${{ matrix.arch }} + path: wheelhouse/*.whl + if-no-files-found: error + + install: + name: installed wheel / py${{ matrix.python.version }} / ${{ matrix.platform.arch }} + needs: wheels + strategy: + fail-fast: false + matrix: + python: + - version: "3.12" + image: python:3.12-slim-bookworm@sha256:392307d22300de8b5986851a12d9176dfc0fc073e65bf6523ebd7dcbeb23564e + - version: "3.13" + image: python:3.13-slim-bookworm@sha256:2325bb286ec344af3e5898cc224b5844e2707ac6e26b1632516fd3edc84a5e26 + - version: "3.14" + image: python:3.14-slim-bookworm@sha256:82bc3c539b8813ada9d68c63b40158fa002f7f33de9bf3312a3dfdc0620dff56 + platform: + - {runner: ubuntu-24.04, arch: x86_64} + - {runner: ubuntu-24.04-arm, arch: aarch64} + runs-on: ${{ matrix.platform.runner }} + timeout-minutes: 20 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + ref: ${{ inputs.source-ref || github.sha }} + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cuphoton-wheels-${{ matrix.platform.arch }} + path: wheelhouse + - name: Install without a compiler, toolkit, or system CFITSIO + env: + PYTHON_VERSION: ${{ matrix.python.version }} + PYTHON_IMAGE: ${{ matrix.python.image }} + run: | + docker run --rm \ + -e WHEEL_ABI="cp${PYTHON_VERSION/./}" \ + -v "$PWD/wheelhouse:/wheels:ro" \ + -v "$PWD/scripts/wheels:/checks:ro" \ + "$PYTHON_IMAGE" \ + sh -ec ' + set -- /wheels/*-"${WHEEL_ABI}"-*.whl + test "$#" = 1 + python -m pip install --only-binary=:all: "$1" + python -I /checks/test_installed.py --mode base + python -m pip install --only-binary=:all: "$1[io]" + python -m pip check + python -I /checks/test_installed.py --mode native + ' + + runtimes: + name: installed runtimes / py${{ matrix.python.version }} / ${{ matrix.platform.arch }} + needs: wheels + strategy: + fail-fast: false + matrix: + python: + - version: "3.12" + dragon: true + image: python:3.12-slim-bookworm@sha256:392307d22300de8b5986851a12d9176dfc0fc073e65bf6523ebd7dcbeb23564e + - version: "3.13" + dragon: true + image: python:3.13-slim-bookworm@sha256:2325bb286ec344af3e5898cc224b5844e2707ac6e26b1632516fd3edc84a5e26 + # Dragon 0.14.2 has no Python 3.14 wheel or source distribution. + - version: "3.14" + dragon: false + image: python:3.14-slim-bookworm@sha256:82bc3c539b8813ada9d68c63b40158fa002f7f33de9bf3312a3dfdc0620dff56 + platform: + - {runner: ubuntu-24.04, arch: x86_64} + - {runner: ubuntu-24.04-arm, arch: aarch64} + runs-on: ${{ matrix.platform.runner }} + timeout-minutes: 25 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + ref: ${{ inputs.source-ref || github.sha }} + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cuphoton-wheels-${{ matrix.platform.arch }} + path: wheelhouse + - name: Exercise installed cuTile imports and distributed CPU runtimes + env: + PYTHON_VERSION: ${{ matrix.python.version }} + PYTHON_IMAGE: ${{ matrix.python.image }} + WITH_DRAGON: ${{ matrix.python.dragon }} + run: | + mkdir -p stack-reports + docker run --rm --shm-size=2g \ + -e WHEEL_ABI="cp${PYTHON_VERSION/./}" \ + -e WITH_DRAGON \ + -e OMPI_ALLOW_RUN_AS_ROOT=1 \ + -e OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 \ + -e DRAGON_DEFAULT_SEG_SZ=536870912 \ + -e DRAGON_INF_SEG_SZ=268435456 \ + -e CUDA_VISIBLE_DEVICES="" \ + -v "$PWD/wheelhouse:/wheels:ro" \ + -v "$PWD/scripts/wheels:/checks:ro" \ + -v "$PWD/stack-reports:/reports" \ + "$PYTHON_IMAGE" \ + sh -ec ' + apt-get update + apt-get install -y --no-install-recommends \ + openmpi-bin=4.1.4-3+b1 libopenmpi3=4.1.4-3+b1 + rm -rf /var/lib/apt/lists/* + set -- /wheels/*-"${WHEEL_ABI}"-*.whl + test "$#" = 1 + extras="cutile,mpi" + if test "$WITH_DRAGON" = true; then + extras="$extras,dragon" + fi + python -m pip install --only-binary=:all: "$1[$extras]" + python -m pip check + python -I /checks/test_stack.py --mode imports \ + --expect "$extras" --report /reports/imports.json + timeout --kill-after=15s 180s mpiexec --oversubscribe -n 2 \ + python -I /checks/test_stack.py --mode mpi --backend cpu \ + --workers 2 --report /reports/mpi.json + if test "$WITH_DRAGON" = true; then + timeout --kill-after=15s 180s dragon --single-node-override \ + python -I /checks/test_stack.py --mode dragon --backend cpu \ + --workers 2 --report /reports/dragon.json + fi + ' + - name: Record the Python 3.14 Dragon limitation + if: matrix.python.version == '3.14' + run: >- + echo 'Python 3.14: cuTile imports and real MPI workers are required. + Dragon is excluded because upstream publishes no compatible wheel. + These CPU jobs do not qualify GPU execution.' >> "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ always() }} + with: + name: cuphoton-runtime-checks-cp${{ matrix.python.version }}-${{ matrix.platform.arch }} + path: stack-reports/*.json + if-no-files-found: error + + distributions: + needs: [sdist, wheels, install, runtimes] + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + ref: ${{ inputs.source-ref || github.sha }} + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + with: + version: "0.12.1" + enable-cache: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cuphoton-sdist + path: dist + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cuphoton-wheels-x86_64 + path: dist + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cuphoton-wheels-aarch64 + path: dist + - run: python scripts/wheels/check_distributions.py dist + - run: uvx --isolated --from twine==6.2.0 twine check --strict dist/* + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cuphoton-distributions + path: dist/* + if-no-files-found: error + retention-days: 30 + - name: Record source identity and artifact hashes + env: + RELEASE_VERSION: ${{ inputs.release-version }} + run: | + python scripts/wheels/release.py record dist provenance.json + cat provenance.json >> "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cuphoton-build-provenance + path: provenance.json + if-no-files-found: error + retention-days: 30 diff --git a/.gitignore b/.gitignore index db3dc397..54a93fe3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__/ *.pyo *.so *.egg-info/ +/src/cuphoton/_version.py .pytest_cache/ .ruff_cache/ .mypy_cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..facd83f2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +## Unreleased + +### Breaking changes + +- Python 3.11 is no longer supported. Use Python 3.12, 3.13, or 3.14. +- Photutils is now optional and is no longer installed by `pip install cuphoton`. + Install `cuphoton[photometry]` for CPU photometry or `cuphoton[gpu]` for the + combined GPU and photometry dependencies. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a695394f..ecec66f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ Use uv for development environments and dependency locking. The supported GPU profile is CUDA 13. ```bash -uv sync --locked --extra dev --extra torch --extra viz +uv sync --locked --extra dev --extra torch --extra viz --extra photometry uv run --locked --extra dev pre-commit install ``` @@ -60,8 +60,9 @@ For CUDA 13 development: uv sync --locked --extra dev --extra gpu --extra viz ``` -Use the smallest profile that exercises the change. The `cutile` extra is -experimental and supports Python 3.12 and 3.13 only. +Use the smallest profile that exercises the change. The experimental `cutile` +extra supports Python 3.12–3.14; use a CUDA 13.2 or newer TileIR compiler +for the supported setup. The `dragon` extra currently supports Python 3.12 and 3.13. ## Checks @@ -73,7 +74,7 @@ uv lock --check uv run --locked --extra dev pre-commit run --all-files make lint make test-cpu -uv build +make build ``` Validation logs should be clean. If warnings are expected, describe them in the diff --git a/MANIFEST.in b/MANIFEST.in index a65c7ec6..b00237b7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,10 +7,13 @@ exclude AGENTS.md exclude CHANGELOG.md exclude CLAUDE.md exclude RELEASING.md +exclude .coderabbit.yaml .editorconfig .gitattributes .gitignore +global-exclude *.ipynb prune .github prune .gitlab prune scripts include scripts/cuphoton-openmpi-rank-exec +recursive-include scripts/wheels *.sh *.py *.txt include LICENSE include CITATION.cff include Makefile diff --git a/Makefile b/Makefile index 673b6309..3fbde645 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,12 @@ # # SPDX-License-Identifier: Apache-2.0 -.PHONY: sync sync-gpu sync-cutile lock lock-check lint format test test-cpu test-core test-xdr test-xfit test-xfit-real test-xpois test-xscan test-xrep test-xray test-gpu clean-dist build package-check release-check ci-lint ci-test-cpu hooks +.PHONY: sync sync-gpu sync-cutile lock lock-check lint format test test-cpu test-core test-xdr test-xfit test-xfit-real test-xpois test-xscan test-xrep test-xray test-gpu test-cutile clean-dist build package-check wheels release-check ci-lint ci-test-cpu hooks -CPU_EXTRAS = --extra dev --extra torch --extra viz +CPU_EXTRAS = --extra dev --extra torch --extra viz --extra photometry GPU_EXTRAS = --extra dev --extra gpu --extra viz -CORE_EXTRAS = --extra dev -VIZ_EXTRAS = --extra dev --extra viz +CORE_EXTRAS = --extra dev --extra photometry +VIZ_EXTRAS = --extra dev --extra viz --extra photometry UV_RUN = uv run --locked sync: @@ -17,7 +17,7 @@ sync-gpu: uv sync --locked $(GPU_EXTRAS) sync-cutile: - uv sync --locked --python 3.12 $(GPU_EXTRAS) --extra cutile + uv sync --locked $(GPU_EXTRAS) --extra cutile lock: uv lock @@ -66,11 +66,17 @@ test-xray: test-gpu: $(UV_RUN) $(GPU_EXTRAS) pytest +test-cutile: + $(UV_RUN) $(GPU_EXTRAS) --extra cutile pytest tests/xfit tests/xpois + clean-dist: rm -rf dist build: clean-dist - CUPHOTON_XDR_BUILD_EXT=0 uv build + CUPHOTON_XDR_BUILD_EXT=0 uv build --sdist + +wheels: build + uv tool run --from cibuildwheel==4.2.1 cibuildwheel --platform linux --output-dir dist dist/*.tar.gz package-check: build uvx --isolated --from twine==6.2.0 twine check --strict dist/* @@ -79,7 +85,9 @@ release-check: $(MAKE) lock-check $(MAKE) ci-lint $(MAKE) ci-test-cpu - $(MAKE) package-check + $(MAKE) wheels + python scripts/wheels/check_distributions.py dist --arch "$$(uname -m)" + uvx --isolated --from twine==6.2.0 twine check --strict dist/* ci-lint: $(MAKE) lint diff --git a/README.md b/README.md index 54a09ef6..0a362dbc 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ when it falls back to CPU. For a deterministic CPU run: ```bash -uv sync --locked --extra dev --extra torch --extra viz +uv sync --locked --extra dev --extra torch --extra viz --extra photometry uv run python examples/run_quickstarts.py --profile cpu ``` @@ -70,9 +70,10 @@ instructions and require a CUDA 13-capable NVIDIA GPU and XDR's native extension xDataReader (`cuphoton.xdr`) reads selected images from local FITS files, decodes supported compression, and applies byte-order and scaling rules to produce CuPy arrays. Use it to feed a GPU workflow while retaining the headers -and scientific metadata in your application. Its native FITS extension -requires a source build. Whether reads use native GPUDirect Storage depends -on the storage and driver configuration. +and scientific metadata in your application. Linux release wheels include its +native FITS extension; install `cuphoton[io]` for the GPU runtime dependencies. +Whether reads use native GPUDirect Storage depends on the storage and driver +configuration. ### xRep: put images on the same sky grid @@ -132,41 +133,61 @@ explains the scientific and file-format terms used here. The base install contains the shared CPU data and scientific stack. Optional extras are deliberately separated by purpose: -Python 3.11 through 3.14 is supported on Linux for the base, GPU, CPU PyTorch, -and visualization profiles. The experimental cuTile profile supports Python -3.12 and 3.13. +CPython 3.12 through 3.14 is supported on Linux, including the experimental +cuTile backend. Dragon currently requires Python 3.12 or 3.13 because its +upstream release has no Python 3.14 wheel. | Extra | Use | | --- | --- | | `dev` | Tests, formatting, linting, and build tools | +| `photometry` | Photutils source detection, backgrounds, and aperture measurements | +| `io` | CUDA 13 CuPy, KvikIO, cuFile, and nvCOMP for XDR | | `torch` | PyTorch workflows that can be forced to CPU execution | -| `gpu` | CUDA 13 PyTorch, CuPy, Numba-CUDA, KvikIO, and nvCOMP backends | -| `cutile` | Experimental `cuda.tile` backend on Python 3.12 or 3.13 | +| `gpu` | The `io` and `photometry` extras plus CUDA 13 PyTorch and Numba-CUDA | +| `cutile` | Experimental `cuda.tile` backend and CuPy | +| `mpi` | mpi4py bindings for an existing MPI runtime | +| `dragon` | DragonHPC runtime on Python 3.12 or 3.13 | | `viz` | Bokeh reviews and Pillow image outputs | +Linux x86-64 and ARM64 wheels include the native XDR extension and a private, +thread-safe CFITSIO library. After the first PyPI release is published, install +it with the commands below. Until then, use the checkout instructions above: + +```bash +python -m pip install cuphoton # CPU data workflows +python -m pip install 'cuphoton[io]' # GPU FITS loading +python -m pip install 'cuphoton[gpu]' # CuPy, Numba, PyTorch, I/O and photometry +python -m pip install 'cuphoton[gpu,mpi]' # Also install MPI Python bindings +python -m pip install 'cuphoton[gpu,dragon]' # Python 3.12 or 3.13 +``` + +The `io` profile needs a CUDA 13-compatible NVIDIA driver, but no compiler, +system CFITSIO, or locally installed CUDA toolkit. On ARM64, Photutils currently +builds from source; `photometry` and `gpu` therefore need a C compiler. +Free-threaded Python, Windows, and macOS wheels are not provided. + Typical editable installs are: ```bash # CPU development -python -m pip install -e '.[dev,torch,viz]' +python -m pip install -e '.[dev,torch,viz,photometry]' # CUDA 13 development python -m pip install -e '.[dev,gpu,viz]' ``` -The cuTile profile is separate because it has a narrower Python and toolchain -compatibility range: +For the cuTile profile, use a CUDA 13.2 or newer TileIR compiler +(`tileiras`). See [compiler and distributed runtime setup](docs/getting-started.md#optional-runtimes). ```bash -uv sync --locked --python 3.12 --extra dev --extra gpu --extra cutile +uv sync --locked --extra dev --extra gpu --extra cutile ``` Only CUDA 13 dependency variants are supported by this release. -xDataReader's GPU FITS path uses a native extension built from source. -From a source checkout, build the extension with -`bash src/cuphoton/xdr/src/build.sh` (see -[docs/components/xdr.md](docs/components/xdr.md)). +Source checkouts require an explicit native XDR build. See the +[XDR installation guide](docs/components/xdr.md) and +[native wheel build and release procedure](docs/packaging.md). ## Python and command-line interfaces @@ -210,7 +231,7 @@ workflow to new products. uv lock --check make lint make test-cpu -uv build +make build ``` See [Contributing](CONTRIBUTING.md) for the full development workflow and diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 235c499a..3f767e77 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -2,12 +2,19 @@ cuPhoton does not intentionally vendor third-party source code. Python runtime, optional-development, and build requirements are declared in -[`pyproject.toml`](pyproject.toml). Optional distributed runtimes and native -system requirements are documented separately below. `uv.lock` records the -reproducible resolution for the project Python dependency profiles; it does -not include the separately installed DragonHPC, `mpi4py`, or MPI runtimes. -Build-system requirements are resolved separately by the PEP 517 build -frontend and are not locked by `uv.lock`; they are labeled `not locked` below. +[`pyproject.toml`](pyproject.toml). Optional distributed runtimes, native +requirements, and the CFITSIO library bundled in Linux wheels are documented +below. `uv.lock` records the reproducible resolution for the project Python +dependency profiles, including the optional DragonHPC and mpi4py packages. +The site-provided MPI implementation is not part of that lock. Build-system requirements are resolved separately +by the PEP 517 build frontend and are not locked by `uv.lock`; they are labeled +`not locked` below. + +Git-derived package versions use the MIT-licensed build tools +`setuptools-scm==10.3.4` and its `vcs-versioning` dependency. They are not +included in the installed runtime dependencies. Native wheel builds pin both +tools in `scripts/wheels/build-requirements.txt`; isolated source builds +resolve build requirements separately from `uv.lock`. cuPhoton uses `uv` to resolve Python distributions from the registries recorded in `uv.lock` (currently the Python Package Index). NVIDIA-authored @@ -19,8 +26,7 @@ incomplete. ## Direct Python dependency inventory -The locked versions below reflect `uv.lock`. `scipy` resolves to 1.17.1 on -Python 3.11 and 1.18.0 on Python 3.12 or later. Compound expressions and +The locked versions below reflect `uv.lock`. Compound expressions and component caveats are retained where binary wheels contain material under more than one license. @@ -33,25 +39,26 @@ more than one license. | `base` | `numexpr>=2.10` | `2.14.1` | `MIT` | [NumExpr](https://github.com/pydata/numexpr) | `uv / PyPI` | | `base` | `numpy>=2.0,<2.6` | `2.4.6` | `BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0` | [NumPy](https://github.com/numpy/numpy) | `uv / PyPI` | | `base` | `pandas>=2.2` | `3.0.3` | `BSD-3-Clause` | [pandas](https://github.com/pandas-dev/pandas) | `uv / PyPI` | -| `base` | `photutils>=3.0` | `3.0.0` | `BSD-3-Clause` | [Photutils](https://github.com/astropy/photutils) | `uv / PyPI` | +| `photometry` | `photutils>=3.0` | `3.0.0` | `BSD-3-Clause` | [Photutils](https://github.com/astropy/photutils) | `uv / PyPI` | | `base` | `pyarrow>=23.0` | `24.0.0` | `Apache-2.0`; binary distributions include Arrow and third-party notices | [Apache Arrow](https://github.com/apache/arrow) | `uv / PyPI` | | `base` | `PyYAML>=6.0` | `6.0.3` | `MIT` | [PyYAML](https://github.com/yaml/pyyaml) | `uv / PyPI` | -| `base` | `scipy>=1.13` | `1.17.1, 1.18.0` | `BSD-3-Clause`; distributions include separately licensed components | [SciPy](https://github.com/scipy/scipy) | `uv / PyPI` | +| `base` | `scipy>=1.13` | `1.18.0` | `BSD-3-Clause`; distributions include separately licensed components | [SciPy](https://github.com/scipy/scipy) | `uv / PyPI` | | `torch` | `torch>=2.13,<3` | `2.13.0` | `Apache-2.0 AND Apache-2.0 WITH LLVM-exception AND BSD-2-Clause AND BSD-3-Clause AND BSL-1.0 AND MIT` | [PyTorch](https://github.com/pytorch/pytorch) | `uv / PyPI` | | `viz` | `bokeh>=3.9` | `3.9.1` | `BSD-3-Clause` | [Bokeh](https://github.com/bokeh/bokeh) | `uv / PyPI` | | `viz` | `pillow>=10.4` | `12.3.0` | `MIT-CMU` | [Pillow](https://github.com/python-pillow/Pillow) | `uv / PyPI` | | `viz` | `tornado>=6.5.10` | `6.5.10` | `Apache-2.0` | [Tornado](https://github.com/tornadoweb/tornado) | `uv / PyPI` | -| `gpu` | `cupy-cuda13x[ctk]>=14,<15` | `14.1.1` | `MIT`; the `ctk` extra installs separately licensed NVIDIA CUDA component wheels | [CuPy](https://github.com/cupy/cupy) | `uv / PyPI` | -| `gpu` | `kvikio-cu13>=26.6,<27` | `26.6.0` | `Apache-2.0` | [KvikIO](https://github.com/rapidsai/kvikio) | `uv / PyPI` | -| `gpu` | `libkvikio-cu13>=26.6,<27` | `26.6.0` | `Apache-2.0` | [KvikIO](https://github.com/rapidsai/kvikio) | `uv / PyPI` | +| `io`, `gpu` | `cupy-cuda13x[ctk]>=14,<15` | `14.1.1` | `MIT`; the `ctk` extra installs separately licensed NVIDIA CUDA component wheels | [CuPy](https://github.com/cupy/cupy) | `uv / PyPI` | +| `io`, `gpu` | `kvikio-cu13==26.6.*` | `26.6.0` | `Apache-2.0` | [KvikIO](https://github.com/rapidsai/kvikio) | `uv / PyPI` | +| `io`, `gpu` | `libkvikio-cu13==26.6.*` | `26.6.0` | `Apache-2.0` | [KvikIO](https://github.com/rapidsai/kvikio) | `uv / PyPI` | | `gpu` | `numba>=0.61,<0.66` | `0.65.1` | `BSD-2-Clause` | [Numba](https://github.com/numba/numba) | `uv / PyPI` | | `gpu` | `numba-cuda[cu13]>=0.30,<0.31` | `0.30.3` | `BSD-2-Clause` | [Numba-CUDA](https://github.com/NVIDIA/numba-cuda) | `uv / PyPI` | -| `gpu` | `nvidia-libnvcomp-cu13>=5.2,<6` | `5.2.0.13` | NVIDIA License Agreement for Software Development Kits; no SPDX expression declared | [nvCOMP](https://developer.nvidia.com/nvcomp) | `uv / PyPI; NVIDIA SDK wheel` | -| `gpu` | `nvidia-nvcomp-cu13>=5.2,<6` | `5.2.0.13` | NVIDIA License Agreement for Software Development Kits; no SPDX expression declared | [nvCOMP](https://developer.nvidia.com/nvcomp) | `uv / PyPI; NVIDIA SDK wheel` | -| `gpu` | `pybind11>=2.12,<4` | `3.0.4` | `BSD-3-Clause` | [pybind11](https://github.com/pybind/pybind11) | `uv / PyPI` | +| `io`, `gpu` | `nvidia-libnvcomp-cu13==5.2.*` | `5.2.0.13` | NVIDIA License Agreement for Software Development Kits; no SPDX expression declared | [nvCOMP](https://developer.nvidia.com/nvcomp) | `uv / PyPI; NVIDIA SDK wheel` | +| `io`, `gpu` | `nvidia-nvcomp-cu13==5.2.*` | `5.2.0.13` | NVIDIA License Agreement for Software Development Kits; no SPDX expression declared | [nvCOMP](https://developer.nvidia.com/nvcomp) | `uv / PyPI; NVIDIA SDK wheel` | +| `native build` | `pybind11==3.0.4` | `build recipe` | `BSD-3-Clause` | [pybind11](https://github.com/pybind/pybind11) | `uv / PyPI` | | `gpu` | `torch>=2.13,<3` | `2.13.0` | `Apache-2.0 AND Apache-2.0 WITH LLVM-exception AND BSD-2-Clause AND BSD-3-Clause AND BSL-1.0 AND MIT` | [PyTorch](https://github.com/pytorch/pytorch) | `uv / PyPI` | -| `cutile` | `cuda-tile>=1.4` | `1.4.0` | `Apache-2.0` | [CUDA Tile](https://github.com/NVIDIA/cutile-python) | `uv / PyPI` | +| `cutile` | `cuda-tile>=1.6,<2` | `1.6.0` | `Apache-2.0` | [CUDA Tile](https://github.com/NVIDIA/cutile-python) | `uv / PyPI` | | `cutile` | `cupy-cuda13x[ctk]>=14,<15` | `14.1.1` | `MIT`; the `ctk` extra installs separately licensed NVIDIA CUDA component wheels | [CuPy](https://github.com/cupy/cupy) | `uv / PyPI` | +| `dev` | `setuptools>=83.0.0` | `83.0.0` | `MIT` | [setuptools](https://github.com/pypa/setuptools) | `uv / PyPI` | | `dev` | `pre-commit>=4.0` | `4.6.0` | `MIT` | [pre-commit](https://github.com/pre-commit/pre-commit) | `uv / PyPI` | | `dev` | `pytest>=8.3` | `9.1.1` | `MIT` | [pytest](https://github.com/pytest-dev/pytest) | `uv / PyPI` | | `dev` | `ruff>=0.15.12` | `0.15.20` | `MIT` | [Ruff](https://github.com/astral-sh/ruff) | `uv / PyPI` | @@ -59,10 +66,11 @@ more than one license. ## Optional distributed runtime inventory The Dragon executor requires DragonHPC. MPI collective aggregation requires -`mpi4py` and an MPI implementation. These dependencies are installed -separately; cuPhoton does not impose numeric version constraints on these -runtimes. The versions below identify the distributions examined for this -inventory; they do not establish compatibility with every Python version, +`mpi4py` and an MPI implementation. The `dragon` extra declares +`dragonhpc>=0.14.2,<0.15`; the `mpi` extra declares `mpi4py>=4.1.2,<5`. +Both are installed from upstream distributions and recorded in `uv.lock`; +neither is bundled in the cuPhoton wheel. The MPI implementation remains +site-provided. The inventoried versions do not establish compatibility with every Python version, transport, or cluster configuration. See the [XPOIS launch documentation](docs/components/xpois.md#launch-with-dragon) for a Dragon launch example. @@ -111,16 +119,23 @@ their bundled and linked components for the selected artifact and transport. Use the license and notice files from the exact installed distributions when preparing a deployment or redistribution inventory. -## Native system dependency inventory +The native release build pins its tools and CUDA 13.0 SDK inputs in +[`scripts/wheels/build-requirements.txt`](scripts/wheels/build-requirements.txt). +Those inputs are separate from the runtime lock. cuFile is requested explicitly +through `cuda-toolkit[cufile]>=13,<14` in the `io` extra. GPU runtime shared +libraries remain in their upstream distributions and are not copied into +cuPhoton wheels. + +## Native dependency inventory | Package | Version or version range | License identifier | Upstream | Use in cuPhoton | Distribution | | --- | --- | --- | --- | --- | --- | -| `CFITSIO` | No numeric version constraint is currently enforced; release validation used `4.6.4`. A thread-safe/reentrant build is required. | [`CFITSIO`](https://spdx.org/licenses/CFITSIO.html) | [NASA HEASARC CFITSIO](https://heasarc.gsfc.nasa.gov/docs/software/fitsio/fitsio.html) | FITS header, HDU, binary-table, and heap-descriptor parsing used to construct native read plans for `cuphoton.xdr`. CFITSIO does not perform the GDS data transfer or GPU decompression. | System- or user-provided native library linked by the `cuphoton.xdr` extension; CFITSIO source is not vendored. A distributor that bundles CFITSIO must retain its copyright notice and warranty disclaimer. | +| `CFITSIO` | Release wheels bundle `4.7.0`, built with reentrant support. Source builds require a reentrant system or user-provided library. | [`CFITSIO`](https://spdx.org/licenses/CFITSIO.html) | [NASA HEASARC CFITSIO](https://heasarc.gsfc.nasa.gov/docs/software/fitsio/fitsio.html) | FITS header, HDU, binary-table, and heap-descriptor parsing used to construct native read plans for `cuphoton.xdr`. CFITSIO does not perform the GDS data transfer or GPU decompression. | Linux wheels include a privately renamed shared library in `cuphoton.libs`; its copyright and warranty disclaimer follow below. The source archive includes a checksum-pinned download/build recipe, not CFITSIO source. | ### CFITSIO copyright and license notice -The following notice is reproduced from the CFITSIO 4.6.4 distribution used -for release validation: +The following notice is reproduced from `licenses/License.txt` in the +CFITSIO 4.7.0 distribution bundled in release wheels: ```text Copyright (Unpublished--all rights reserved under the copyright laws of @@ -185,13 +200,13 @@ For a CPU-only development environment, replace `gpu` with `torch`. ## cuPhoton distribution contents -`make build` creates a pure-Python wheel with the xDataReader native extension -disabled (`CUPHOTON_XDR_BUILD_EXT=0`) and a source distribution containing -cuPhoton's own extension sources. Both artifacts include `LICENSE` and this -notice file. Dependencies listed here are installed separately; these builds -do not bundle DragonHPC, `mpi4py`, MPI, CFITSIO, or CUDA libraries. +`make build` creates a source distribution containing cuPhoton's extension +sources and the pinned native build recipe. `make wheels` builds the native +Linux wheels from that archive. Each wheel includes the XDR extension and a +privately renamed CFITSIO shared library, with the license notice above. The +source archive and wheels include `LICENSE` and this notice file. -A source build with the native extension enabled links against the available -CFITSIO and CUDA libraries. A distributor that bundles libraries, including -through static linking, must inventory the resulting artifact and retain the -applicable third-party licenses and notices. +DragonHPC, `mpi4py`, MPI, and CUDA runtime libraries are installed separately. +A development source build links against the available CFITSIO and CUDA +libraries. Distributors must retain the licenses and notices for the libraries +included in their artifacts. diff --git a/docs/components/xdr.md b/docs/components/xdr.md index e1c614b2..a49f04fd 100644 --- a/docs/components/xdr.md +++ b/docs/components/xdr.md @@ -25,28 +25,39 @@ reader by default. ## Install -Install the CUDA 13 development profile: +After the first PyPI release is published, install the I/O profile for GPU +FITS loading. Until then, use the source checkout instructions below: ```bash -uv sync --locked --extra dev --extra gpu +python -m pip install 'cuphoton[io]' ``` -The base package supports imports in CPU environments. xDataReader's loading -paths require the `gpu` extra. To build its native extension after syncing -the GPU environment, run: +Linux x86-64 and ARM64 wheels for CPython 3.12–3.14 include the native +extension and a private, reentrant CFITSIO 4.7.0 library. The extra installs +CuPy, KvikIO, cuFile, and nvCOMP; `gpu` also includes these dependencies. +The base package remains importable without GPU dependencies. No compiler, +local CUDA toolkit, or system CFITSIO is needed for a wheel installation. +Only CUDA 13 dependency variants are supported. A compatible NVIDIA driver +is required for GPU execution. + +GPUDirect Storage also needs a supported host driver, filesystem, and storage +configuration. KvikIO compatibility mode supports ordinary local file I/O; +installing a wheel does not configure GDS. Use `KVIKIO_COMPAT_MODE=ON` to +select compatibility mode explicitly. + +For development from a source checkout: ```bash +uv sync --locked --extra dev --extra io bash src/cuphoton/xdr/src/build.sh ``` -The native extension also needs CUDA toolkit headers, cuFile headers, and a -thread-safe CFITSIO development install visible through `pkg-config cfitsio` or -`CUPHOTON_XDR_CFITSIO_ROOT`. - -Normal PEP 517 and pip builds produce the pure Python package. Set -`CUPHOTON_XDR_BUILD_EXT=1` for a native source build, as `build.sh` does above. -`CUPHOTON_XDR_BUILD_EXT=0` explicitly selects the default pure Python build. -The supported dependency variants target CUDA 13. +Source and editable builds default to a Python-only installation without +probing native prerequisites. Building the extension requires +`CUPHOTON_XDR_BUILD_EXT=1`, as performed by `build.sh` above. The source build +requires a C++17 compiler, CUDA and cuFile headers, and a reentrant CFITSIO +development installation. See [the wheel build procedure](../packaging.md) +for the pinned release recipe. ### Native extension availability @@ -84,13 +95,16 @@ tar -xzf cfitsio-4.7.0.tar.gz Return to the checkout and run `build.sh` in the same shell so the prefix remains exported. The prefix must contain `include/fitsio.h` and the CFITSIO library in `lib` or `lib64`. `--disable-curl` removes CFITSIO's optional URL -support. Keep `--enable-reentrant` for concurrent native planning and reads. +support; local FITS loading does not require it. Keep `--enable-reentrant` +for concurrent native planning and reads. -Official release wheels are pure Python (`py3-none-any`). Build -`cuphoton.xdr._nvcomp_batch_ext` from source using the installed `gpu` -environment: `build.sh` passes `--no-build-isolation` to resolve pybind11, -KvikIO, and nvCOMP from that environment. CFITSIO headers and libraries come from +An explicit native source build runs without +PEP 517 build isolation (as `build.sh` does with `--no-build-isolation`) +because pybind11, KvikIO and nvCOMP are resolved from the installed `io` +environment. CFITSIO headers and libraries come from `CUPHOTON_XDR_CFITSIO_ROOT` or `pkg-config cfitsio`, as described above. +The helper installs pybind11 as a build dependency; it is not an I/O runtime +requirement. Verify the extension after building: ```bash diff --git a/docs/components/xpois.md b/docs/components/xpois.md index 1f8afbc8..17b99ed0 100644 --- a/docs/components/xpois.md +++ b/docs/components/xpois.md @@ -412,35 +412,40 @@ uv run cuphoton xpois help fit-batch The Dragon executor requires [DragonHPC](https://dragonhpc.github.io/dragon/doc/_build/html/index.html) (Python distribution `dragonhpc`, import `dragon`) in the same Python -environment as cuPhoton on every participating node. Install DragonHPC -separately alongside cuPhoton's `gpu` extra and manage its version as an -external runtime dependency. - -For example, install the released DragonHPC 0.14.2 package into a CUDA 13 -cuPhoton environment: +environment as cuPhoton on every participating node. The `dragon` extra +installs the runtime and keeps it in the project lock: ```bash -uv sync --locked --python 3.12 --extra gpu -uv pip install --python .venv/bin/python "dragonhpc==0.14.2" +uv sync --locked --python 3.13 --extra gpu --extra dragon ``` +For a published wheel, use `python -m pip install 'cuphoton[gpu,dragon]'`. The [DragonHPC 0.14.2 wheels](https://pypi.org/project/dragonhpc/0.14.2/#files) -support CPython 3.11 through 3.13 on Linux x86-64 and AArch64 with glibc 2.28 -or newer. Use one of those Python versions for this installation. +support cuPhoton's Python 3.12 and 3.13 environments on Linux x86-64 and +AArch64 with glibc 2.28 or newer. Dragon has no Python 3.14 wheel yet; +requesting the extra there fails installation rather than silently omitting it. Use the installed `.venv/bin/dragon` launcher with the examples below. -`uv sync` removes packages outside the project lock, so repeat the DragonHPC -installation after resynchronizing the environment. Use the same cuPhoton -environment and DragonHPC version on every node. Each run records the -DragonHPC version it discovers. See the +Keep `--extra dragon` when resynchronizing a checkout environment. Use the +same cuPhoton environment and DragonHPC version on every node. Each run +records the DragonHPC version it discovers. See the [runtime notices](../../THIRD_PARTY_NOTICES.md#optional-distributed-runtime-inventory) for licensing and installation details. The MPI executor uses an external MPI or scheduler launcher. Collective -aggregation (`--aggregation-mode mpi`) also requires `mpi4py` built for the -selected MPI implementation. Shared-file aggregation exchanges results -through the filesystem, with the external launcher managing its processes. -Install the runtimes required by the selected executor on each node. +aggregation (`--aggregation-mode mpi`) also requires the `mpi` extra: + +```bash +uv sync --locked --extra gpu --extra mpi +# Or, for a published wheel: +python -m pip install 'cuphoton[gpu,mpi]' +``` + +The extra installs mpi4py, while the MPI implementation and matching launcher +remain site-provided. Follow the [mpi4py installation instructions](https://mpi4py.readthedocs.io/en/stable/install.html) +when a site-specific build is needed. Shared-file aggregation does not import +mpi4py: it exchanges results through the filesystem, with the external +launcher managing processes. Install the selected runtime on every node. ### Manifest and storage contract diff --git a/docs/getting-started.md b/docs/getting-started.md index a8c88415..c3656cb5 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -2,16 +2,36 @@ ## Requirements -cuPhoton supports Python 3.11 through 3.14 on Linux for the base, GPU, CPU -PyTorch, and visualization profiles. CPU workflows run with the base or CPU -PyTorch dependencies. The GPU profile targets CUDA 13 and requires a -compatible NVIDIA driver. Python 3.12 or 3.13 is required for the -experimental cuTile profile. +cuPhoton supports CPython 3.12 through 3.14 on Linux for the base, GPU, CPU +PyTorch, and visualization profiles. CPU workflows do not require CUDA. The +GPU profile targets CUDA 13 and requires a compatible NVIDIA driver. The +experimental cuTile profile supports the same Python versions. Dragon +currently requires Python 3.12 or 3.13. Install [uv](https://docs.astral.sh/uv/) before working from a checkout. uv is the supported environment and lock-file tool; editable pip installation is also available for integration into an existing environment. +## Install a release + +After the first PyPI release is published, install it with the commands below. +Until then, use the [checkout instructions](#clone-and-select-a-profile). + +```bash +python -m pip install cuphoton +python -m pip install 'cuphoton[io]' # Native GPU FITS loading +``` + +The Linux x86-64 and ARM64 wheels include the XDR extension and private +CFITSIO. `io` installs the CUDA 13 runtime dependencies; no compiler or local +CUDA toolkit is needed. GPU execution still requires a compatible NVIDIA +driver. See [XDR](components/xdr.md) for GPUDirect Storage requirements. + +Install `cuphoton[photometry]` for source detection, background estimation, +and aperture photometry. It uses Photutils, which currently requires a C +compiler on ARM64. The broader `gpu` profile includes `io` and `photometry`. +Free-threaded Python and Windows/macOS wheels are not provided. + ## Clone and select a profile ```bash @@ -28,7 +48,7 @@ uv sync --locked --extra dev --extra gpu --extra viz For CPU development, including PyTorch workflows: ```bash -uv sync --locked --extra dev --extra torch --extra viz +uv sync --locked --extra dev --extra torch --extra viz --extra photometry ``` The base package supports CPU data inspection and NumPy/SciPy workflows: @@ -42,18 +62,66 @@ The extras are composable: | Extra | Adds | | --- | --- | | `dev` | pytest, Ruff, pre-commit, and packaging checks | +| `photometry` | Photutils background, detection, and aperture routines | +| `io` | CuPy, KvikIO, cuFile, and nvCOMP for native XDR | | `torch` | CPU-capable PyTorch | -| `gpu` | CUDA 13 PyTorch, CuPy, and Numba-CUDA | +| `gpu` | `io`, `photometry`, CUDA 13 PyTorch, and Numba-CUDA | | `cutile` | experimental `cuda.tile` and its CuPy bridge | +| `mpi` | mpi4py bindings; an MPI runtime and launcher are also required | +| `dragon` | DragonHPC; Python 3.12 or 3.13 | | `viz` | Bokeh and Pillow | For cuTile backend development: ```bash -uv sync --locked --python 3.12 \ - --extra dev --extra gpu --extra cutile +uv sync --locked --extra dev --extra gpu --extra cutile ``` +## Optional runtimes + +Extras compose: use `cuphoton[gpu,mpi]` or `cuphoton[gpu,dragon]` for the +corresponding distributed GPU executor. From a checkout: + +```bash +# Choose the MPI profile: +uv sync --locked --extra gpu --extra mpi +# Or the Dragon profile: +uv sync --locked --python 3.13 --extra gpu --extra dragon +``` + +The `mpi` extra installs mpi4py, not an MPI implementation. Load the site's +MPI module or install a compatible MPI runtime and launcher, then verify +`.venv/bin/python -c 'from mpi4py import MPI; print(MPI.Get_library_version())'` +from a checkout, or use the activated environment's Python for a wheel install. +Use the same runtime with `mpiexec` on every node. Follow +[mpi4py's installation guide](https://mpi4py.readthedocs.io/en/stable/install.html) +for site-specific builds. MPI file aggregation does not import mpi4py. + +DragonHPC 0.14.2 provides Linux x86-64 and ARM64 wheels for Python 3.12 and +3.13, but no Python 3.14 wheel. Installing `[dragon]` on Python 3.14 fails +instead of silently omitting Dragon. Use the installed `dragon` launcher and +the [distributed launch examples](components/xpois.md#launch-with-dragon). +Both executors need the same environment on every participating node. + +The `cutile` extra installs cuda-tile 1.6 or newer and CuPy on Python +3.12–3.14. For kernel compilation in this setup, use a CUDA 13.2 or newer +toolkit with `tileiras`, `ptxas`, and NVVM. The compiler is separate: +cuTile's upstream `[tileiras]` extra requires `cuda-toolkit>=13.2`, while +PyTorch 2.13 pins that Python metapackage to 13.0.3. Requesting both compiler +and PyTorch extras in one environment cannot currently resolve. Use a +system toolkit for cuTile compilation alongside the pip GPU runtime +dependencies; see the [cuTile setup guide](https://docs.nvidia.com/cuda/cutile-python/quickstart.html). + +Select the matching compiler tools for the process, for example: + +```bash +PATH=/usr/local/cuda-13.2/bin:$PATH CUDA_HOME=/usr/local/cuda-13.2 \ + uv run --locked --extra gpu --extra cutile cuphoton --version +``` + +Use the same environment when running cuTile workloads. Selecting the +compiler does not require adding the toolkit's libraries to `LD_LIBRARY_PATH`. + ## Verify the checkout ```bash @@ -72,7 +140,7 @@ The repository uses standard Python package metadata. From an activated environment: ```bash -python -m pip install -e '.[dev,torch,viz]' +python -m pip install -e '.[dev,torch,viz,photometry]' ``` or, for CUDA 13: diff --git a/docs/packaging.md b/docs/packaging.md new file mode 100644 index 00000000..950b3ede --- /dev/null +++ b/docs/packaging.md @@ -0,0 +1,210 @@ +# Native wheels + +Release artifacts are six Linux wheels (CPython 3.12, 3.13, and 3.14 on +x86-64 and ARM64) plus one source archive. Wheels target glibc 2.28 or later. +The runtime dependencies may impose a newer glibc floor; the installed-wheel +CI tests use Debian 12. Free-threaded Python, Windows, macOS, and conda builds +are outside this matrix. + +Each wheel contains `cuphoton.xdr._nvcomp_batch_ext` and a privately renamed, +reentrant CFITSIO 4.7.0 shared library. CUDA, cuFile, KvikIO, and nvCOMP remain +in their upstream wheels, installed through `cuphoton[io]`. `cuphoton[gpu]` +also includes the photometry, PyTorch, and Numba backends. Photutils currently +requires a source build and C compiler on ARM64; `cuphoton` and `cuphoton[io]` +do not install it. + +The native KvikIO ABI is restricted to the 26.6 release family. nvCOMP is +restricted to 5.2. Updating either family requires rebuilding and qualifying +the native wheels. The CUDA SDK build inputs are pinned to 13.0 so a newer +build environment cannot silently raise the runtime floor. + +## Versions and release candidates + +The build derives the package version from Git tags using `setuptools-scm`: + +| Tag | Installed version | +| --- | --- | +| `v0.1.3rc0` | `0.1.3rc0` | +| `v0.1.3rc1` | `0.1.3rc1` | +| `v0.1.3` | `0.1.3` | + +Use exactly `vX.Y.Z` or `vX.Y.ZrcN`. A checkout after a release tag receives +a development version. There is no version constant to bump: builds generate +`cuphoton/_version.py`, and package metadata, `cuphoton.__version__`, and +`cuphoton --version` use that version. A source archive preserves it without +Git. An unbuilt source checkout reports `0.0.0.dev0` until installed or built; +a Git-free copy without archive metadata cannot produce a release. + +Release CI explicitly selects the triggering tag's version. This also works +when an RC tag and a final tag refer to the same commit. A final release needs +a new build and qualification because its version and metadata change; do not +rename RC wheels. + +An exact RC pin works without `--pre`: + +```bash +python -m pip install 'cuphoton[dev,gpu,io,photometry]==0.1.3rc0' +``` + +`gpu` already includes `io` and `photometry`, so `[dev,gpu]` is equivalent. +Add `viz` for visualization dependencies. Use `--pre` when selecting the newest +available prerelease instead of pinning one. Each changed candidate needs a +new RC number: PyPI does not allow replacing an uploaded filename. + +## Build + +From the checkout, on each native Linux architecture with Docker and uv: + +```bash +make wheels +python scripts/wheels/check_distributions.py dist --arch "$(uname -m)" +uvx --from twine==6.2.0 twine check --strict dist/* +``` + +`make wheels` builds a source archive, then uses cibuildwheel 4.2.1 to build +all three Python ABIs from that archive. It uses digest-pinned manylinux +images, the build inputs in `scripts/wheels/build-requirements.txt`, and the +checksum-pinned CFITSIO recipe in `scripts/wheels/prepare_cfitsio.sh`. +CFITSIO's upstream tests run before installation. No host CUDA toolkit is +used. Release builds must not set `CIBW_TEST_SKIP`. + +The default `make build` produces only the source archive. Plain source and +editable installs remain Python-only unless `CUPHOTON_XDR_BUILD_EXT=1` is +set. See [XDR source installation](components/xdr.md#native-extension-availability) +for the explicit native development build. + +The reusable `wheels.yml` workflow builds from one source archive on native +x86-64 and ARM64 runners. It checks base imports inside cibuildwheel, then +installs each wheel and its `io` dependencies in a clean Python container +with no compiler, system CFITSIO, or CUDA toolkit. Native loading and CFITSIO +planning must work without a GPU. The final artifact check requires exactly +six native wheels and one source archive; it rejects accidental pure wheels, +missing native code or notices, and bundled GPU runtime libraries. + +The installed-wheel runtime matrix also requires cuTile imports and two real +MPI workers on every Python/architecture pair. Python 3.12 and 3.13 require +two real Dragon workers as well; upstream Dragon has no Python 3.14 wheel. +These workers solve generated xPOIS inputs on the CPU and check numerical +results, distinct processes, and MPI collectives. JSON receipts are retained +as CI artifacts. These checks do not establish GPU executor correctness. + +## GPU qualification + +CI CPU checks do not establish GPU correctness. Download the exact +`cuphoton-distributions` artifact and test each wheel on its architecture and +Python version with a CUDA 13-compatible driver. Use a clean runtime container +without a compiler, system CFITSIO, or a local toolkit. Install the wheel +with its `io` extra, then run from outside any checkout: + +```bash +python -m pip install '/artifacts/cuphoton--.whl[io]' +python -m pip check +python -I /checks/test_installed.py --mode gpu --output /results/gpu.json +``` + +`test_installed.py` lives under `scripts/wheels` in the source archive. The +GPU check requires native loading and decoding, compares generated FITS +images with Astropy, and exercises ordering, ROI, streaming, caller-owned +outputs, concurrency, and buffer release. Missing GPU/native capabilities +fail the check. It forces KvikIO compatibility I/O, so it does not qualify +GPUDirect Storage. Qualify GDS separately on suitable host/storage systems. + +Retain the source commit, artifact SHA256 values, container image, installed +dependency versions, Python, GPU/driver details, and JSON test receipts. When +testing the minimum CUDA runtime, constrain `cuda-toolkit==13.0.3.0` and +`nvidia-nvjitlink==13.0.88`; also test the normal unconstrained `io` resolution. + +## Whole-stack qualification + +Use `scripts/wheels/test_stack.py` alongside the native GPU check above. +Install the same wheel with `[gpu,viz,cutile,mpi,dragon]` on Python 3.12 and +3.13, or `[gpu,viz,cutile,mpi]` on Python 3.14. An MPI runtime and matching +launcher are required. cuTile uses an external CUDA 13.2 or newer compiler; +see [optional runtime setup](getting-started.md#optional-runtimes) for the +current PyTorch/compiler dependency constraint. + +From outside the checkout, using the installed environment's Python: + +```bash +python -I /checks/test_installed.py --mode gpu --output /results/xdr.json +python -I /checks/test_stack.py --mode gpu --report /results/compute.json + +# One rank per visible GPU; this example needs two CUDA 13-capable GPUs. +CUDA_VISIBLE_DEVICES=0,1 timeout --kill-after=15s 180s mpiexec -n 2 \ + cuphoton-openmpi-rank-exec -- python -I /checks/test_stack.py \ + --mode mpi --backend cupy --workers 2 --report /results/mpi-gpu.json + +# Python 3.12 or 3.13; two available GPU placements are required. +CUDA_VISIBLE_DEVICES=0,1 timeout --kill-after=15s 180s dragon --single-node-override \ + python -I /checks/test_stack.py --mode dragon --backend cupy \ + --workers 2 --report /results/dragon-gpu.json +``` + +For a one-GPU host, use one visible device, `mpiexec -n 1`, and `--workers 1` +for both executors. Record that as single-worker acceptance, not multi-GPU +qualification. Use `--backend cpu --workers 2` with the two launchers to +repeat the CPU runtime checks without GPU requirements. + +The compute check solves the same known xPOIS problem with CPU, CuPy, +Numba-CUDA, and cuTile, and checks CuPy-to-PyTorch GPU inference through +xScan's DLPack bridge. The executor checks run cuPhoton's real MPI/Dragon +batch paths and verify saved numerical outputs. Missing selected runtimes, +GPU support, worker results, or compiler tools fail instead of skipping. +Retain these JSON receipts with the wheel hashes and native XDR receipts. + +## Publish the qualified artifacts + +Configure these Trusted Publishers in the respective package-index accounts: + +| Setting | PyPI | TestPyPI | +| --- | --- | --- | +| Project | `cuPhoton` | `cuPhoton` | +| Repository owner | `NVIDIA` | `NVIDIA` | +| Repository | `cuPhoton` | `cuPhoton` | +| Workflow filename | `publish.yml` | `publish.yml` | +| GitHub environment | `pypi` | `testpypi` | + +Require at least one reviewer other than the release operator for each GitHub +environment and enable **Prevent self-review**. Allow deployments from branch +`main` and tags matching `v*`. Protect release tags with a ruleset that restricts +creation to authorized release maintainers and prevents tag updates and deletion. +Project owners register each publisher on its index; GitHub environment +configuration alone does not grant upload access. +No stored API token is needed. See the [PyPI Trusted Publisher setup +instructions](https://docs.pypi.org/trusted-publishers/adding-a-publisher/). + +Pushing `v0.1.3rc0` or `v0.1.3` starts `publish.yml`. It validates the tag and +requires its commit to belong to `main` or `0.1.x`, builds the six native wheels +from one versioned source archive, and tests their clean installation. It then +waits at the `testpypi` environment for approval. PyPI publication requires an +explicit manual dispatch with `target=pypi`. No release tags are created by the +workflow. + +1. Download `cuphoton-distributions` and `cuphoton-build-provenance` from the + release run. The latter records the tag, source and workflow commits, build + run/attempt, and every distribution's SHA256. Artifacts expire after 30 days. +2. Qualify those exact binaries on both GPU architectures as described above. + The environment reviewer checks those results and hashes before approving + the upload. CPU CI success does not establish GPU correctness. +3. Approve the tag-triggered TestPyPI upload and verify its downloads against + the recorded hashes. +4. Dispatch `publish.yml` from `main` with the same `version`, `target=pypi`, + and the original release `run-id`. This promotes the identical files and + requires a separate approval from a non-operator reviewer on `pypi`. + +Publishing runs for one version and destination are serialized. Cancel an +existing waiting run before dispatching a replacement for that destination. + +Manual dispatch without `run-id` builds the supplied existing tag and publishes +to the selected environment after approval. Dispatch with `run-id` always uses +the original **build run**, including when its upload failed or was cancelled; +a promotion-only run has no build artifacts of its own. To rebuild, start a +new dispatch; uploaded artifacts are immutable, so rerunning an already +completed build in the same run cannot replace them. The source tag must +still resolve to the recorded commit, and version, artifact hashes, matrix, +and the build attempt's successful validation must all match. + +A retry first compares files already on the selected index with the retained +artifacts. Matching files are omitted from the upload; a differing hash or +unexpected filename fails. Publication never rebuilds artifacts or silently +accepts different bytes. The upload job alone receives the OIDC permission. diff --git a/pyproject.toml b/pyproject.toml index 97d583b8..f713936f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 [build-system] -requires = ["setuptools>=83.0.0", "wheel"] +requires = ["setuptools>=83.0.0", "setuptools-scm==10.3.4", "wheel"] build-backend = "setuptools.build_meta" [project] @@ -11,7 +11,7 @@ name = "cuphoton" dynamic = ["version"] description = "GPU-accelerated astronomy and imaging tools from NVIDIA." readme = "README.md" -requires-python = ">=3.11,<3.15" +requires-python = ">=3.12,<3.15" license = "Apache-2.0" license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"] authors = [{ name = "NVIDIA Corporation" }] @@ -21,7 +21,6 @@ classifiers = [ "Intended Audience :: Science/Research", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", @@ -33,13 +32,15 @@ dependencies = [ "numexpr>=2.10", "numpy>=2.0,<2.6", "pandas>=2.2", - "photutils>=3.0", "pyarrow>=23.0", "PyYAML>=6.0", "scipy>=1.13", ] [project.optional-dependencies] +photometry = [ + "photutils>=3.0", +] torch = [ "torch>=2.13,<3", ] @@ -48,22 +49,32 @@ viz = [ "pillow>=10.4", "tornado>=6.5.10", ] -gpu = [ +io = [ "cupy-cuda13x[ctk]>=14,<15; platform_system == 'Linux'", - "kvikio-cu13>=26.6,<27; platform_system == 'Linux'", - "libkvikio-cu13>=26.6,<27; platform_system == 'Linux'", + "cuda-toolkit[cufile]>=13,<14; platform_system == 'Linux'", + "kvikio-cu13==26.6.*; platform_system == 'Linux'", + "libkvikio-cu13==26.6.*; platform_system == 'Linux'", + "nvidia-libnvcomp-cu13==5.2.*; platform_system == 'Linux'", + "nvidia-nvcomp-cu13==5.2.*; platform_system == 'Linux'", +] +gpu = [ + "cuphoton[io,photometry]", "numba>=0.61,<0.66; platform_system == 'Linux'", "numba-cuda[cu13]>=0.30,<0.31; platform_system == 'Linux'", - "nvidia-libnvcomp-cu13>=5.2,<6; platform_system == 'Linux'", - "nvidia-nvcomp-cu13>=5.2,<6; platform_system == 'Linux'", - "pybind11>=2.12,<4; platform_system == 'Linux'", "torch>=2.13,<3; platform_system == 'Linux'", ] cutile = [ - "cuda-tile>=1.4; python_version >= '3.12' and python_version < '3.14' and platform_system == 'Linux'", - "cupy-cuda13x[ctk]>=14,<15; python_version >= '3.12' and python_version < '3.14' and platform_system == 'Linux'", + "cuda-tile>=1.6,<2; platform_system == 'Linux'", + "cupy-cuda13x[ctk]>=14,<15; platform_system == 'Linux'", +] +mpi = [ + "mpi4py>=4.1.2,<5", +] +dragon = [ + "dragonhpc>=0.14.2,<0.15", ] dev = [ + "setuptools>=83.0.0", "pre-commit>=4.0", "pytest>=8.3", "ruff>=0.15.12", @@ -83,8 +94,18 @@ package-dir = {"" = "src"} include-package-data = false script-files = ["scripts/cuphoton-openmpi-rank-exec"] -[tool.setuptools.dynamic] -version = { attr = "cuphoton.__version__" } +[tool.setuptools_scm] +version_file = "src/cuphoton/_version.py" +tag_regex = '^v(?P[0-9]+\.[0-9]+\.[0-9]+(?:rc[0-9]+)?)$' +git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "v[0-9]*"] +version_file_template = ''' +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Generated at build time by setuptools-scm. Do not edit. +__version__ = {version!r} +''' [tool.setuptools.packages.find] where = ["src"] @@ -102,7 +123,7 @@ environments = ["sys_platform == 'linux'"] [tool.ruff] line-length = 78 -target-version = "py311" +target-version = "py312" force-exclude = true extend-exclude = [ "*.ipynb", @@ -128,3 +149,17 @@ markers = [ ] pythonpath = ["src"] testpaths = ["tests"] + +[tool.cibuildwheel] +build = "cp312-manylinux_* cp313-manylinux_* cp314-manylinux_*" +build-frontend = { name = "build[uv]", args = ["--no-isolation"] } +before-all = "bash {package}/scripts/wheels/prepare_cfitsio.sh" +before-build = "bash {package}/scripts/wheels/install_build_dependencies.sh" +repair-wheel-command = "python {package}/scripts/wheels/repair_wheel.py {wheel} {dest_dir}" +test-command = "python -I {package}/scripts/wheels/test_installed.py --mode base" + +[tool.cibuildwheel.linux] +archs = ["auto64"] +manylinux-x86_64-image = "quay.io/pypa/manylinux_2_28_x86_64@sha256:ae21cd1c8220f773f9b5934f3b845d677494d4cd4b8981c1e416f654e8afa74e" +manylinux-aarch64-image = "quay.io/pypa/manylinux_2_28_aarch64@sha256:f2c063e4d418c18356490aa38ebd045a8e27a6e3e1f2284dd1fd62da1218789d" +environment = { CUPHOTON_XDR_BUILD_EXT = "1", CUPHOTON_XDR_CFITSIO_ROOT = "/opt/cuphoton-cfitsio", CUDA_HOME = "", CUDA_PATH = "" } diff --git a/scripts/wheels/build-requirements.txt b/scripts/wheels/build-requirements.txt new file mode 100644 index 00000000..e93e431d --- /dev/null +++ b/scripts/wheels/build-requirements.txt @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Native wheel build inputs, installed separately for each CPython ABI. +setuptools==83.0.0 +setuptools-scm==10.3.4 +vcs-versioning==2.5.0 +wheel==0.48.0 +pybind11==3.0.4 +libkvikio-cu13==26.6.0 +rapids-logger==0.2.3 +nvidia-libnvcomp-cu13==5.2.0.13 +# Build against the same CUDA 13.0 SDK floor used by runtime acceptance. +nvidia-cuda-runtime==13.0.96 +nvidia-cuda-crt==13.0.88 +nvidia-cufile==1.15.1.6 +auditwheel==6.8.2 +patchelf==0.19.1.0 +packaging==26.2 +pyelftools==0.32 diff --git a/scripts/wheels/check_distributions.py b/scripts/wheels/check_distributions.py new file mode 100755 index 00000000..f4f95551 --- /dev/null +++ b/scripts/wheels/check_distributions.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Validate the native cuPhoton wheel matrix and source distribution.""" + +from __future__ import annotations + +import argparse +import re +import tarfile +import zipfile +from email.parser import BytesParser +from pathlib import Path + +ABIS = ("cp312", "cp313", "cp314") +ARCHITECTURES = ("x86_64", "aarch64") +LICENSES = {"LICENSE", "THIRD_PARTY_NOTICES.md"} +SOURCES = { + "setup.py", + "pyproject.toml", + "src/cuphoton/xdr/setup_package.py", + "src/cuphoton/xdr/src/nvcomp_batch_ext.cpp", + "src/cuphoton/xdr/src/nvcomp_batch_ext.h", + "src/cuphoton/xdr/src/memory_manager.cpp", + "src/cuphoton/xdr/src/io.cpp", + "src/cuphoton/xdr/src/build.sh", + "scripts/wheels/prepare_cfitsio.sh", + "scripts/wheels/install_build_dependencies.sh", + "scripts/wheels/build-requirements.txt", + "scripts/wheels/repair_wheel.py", + "scripts/wheels/test_installed.py", + "scripts/wheels/check_distributions.py", +} +BINARY = re.compile(r"\.(?:so(?:\.\d+)*|a|o|pyd|dll|dylib|whl|pyc|pyo)$") + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def check_metadata(content, version, artifact, license_expression): + metadata = BytesParser().parsebytes(content) + require(metadata["Name"] == "cuphoton", f"{artifact}: wrong project name") + require(metadata["Version"] == version, f"{artifact}: version mismatch") + require( + metadata.get_all("License-Expression") == [license_expression], + f"{artifact}: expected License-Expression: {license_expression}", + ) + python_bounds = (metadata["Requires-Python"] or "").replace(" ", "") + require( + set(python_bounds.split(",")) == {">=3.12", "<3.15"}, + f"{artifact}: Requires-Python must be >=3.12,<3.15", + ) + require( + {"io", "gpu"} <= set(metadata.get_all("Provides-Extra", [])), + f"{artifact}: missing io/gpu extras", + ) + + +def check_wheel(path): + match = re.fullmatch( + r"cuphoton-([^-]+)-(?:\d[^-]*-)?(cp31[234])-\2-(.+)\.whl", path.name + ) + require(match is not None, f"{path.name}: unexpected wheel filename/ABI") + version, abi, platform = match.groups() + platforms = platform.split(".") + architecture = re.search( + r"(?:^|\.)manylinux_2_28_(x86_64|aarch64)(?:\.|$)", platform + ) + require( + architecture is not None, f"{path.name}: missing manylinux_2_28 tag" + ) + arch = architecture.group(1) + require( + all( + re.fullmatch(rf"manylinux(?:_\d+_\d+|20\d+)_{arch}", tag) + for tag in platforms + ), + f"{path.name}: inconsistent platform architecture", + ) + info = f"cuphoton-{version}.dist-info/" + native = ( + "cuphoton/xdr/_nvcomp_batch_ext." + f"cpython-{abi[2:]}-{arch}-linux-gnu.so" + ) + with zipfile.ZipFile(path) as archive: + files = set(archive.namelist()) + required = {native, info + "METADATA", info + "WHEEL"} + required.update(info + "licenses/" + name for name in LICENSES) + require( + required <= files, + f"{path.name}: missing {sorted(required - files)}", + ) + check_metadata( + archive.read(info + "METADATA"), + version, + path.name, + "Apache-2.0 AND CFITSIO", + ) + wheel = BytesParser().parsebytes(archive.read(info + "WHEEL")) + require( + wheel["Root-Is-Purelib"] == "false", f"{path.name}: pure wheel" + ) + require( + set(wheel.get_all("Tag", [])) + == {f"{abi}-{abi}-{tag}" for tag in platforms}, + f"{path.name}: WHEEL tags disagree with filename", + ) + cfitsio = { + name + for name in files + if re.fullmatch( + r"cuphoton\.libs/libcfitsio-[0-9a-f]{8,}\.so(?:\.\d+)*", name + ) + } + require( + len(cfitsio) == 1, + f"{path.name}: expected one renamed CFITSIO library", + ) + binaries = {name for name in files if BINARY.search(name)} + require( + binaries == {native} | cfitsio, + f"{path.name}: unexpected bundled binaries: " + f"{sorted(binaries - {native} - cfitsio)}", + ) + require( + b"Permission to freely use, copy, modify, and distribute" + in archive.read(info + "licenses/THIRD_PARTY_NOTICES.md"), + f"{path.name}: missing CFITSIO notice", + ) + return (abi, arch), version + + +def check_sdist(path): + match = re.fullmatch(r"cuphoton-(.+)\.tar\.gz", path.name) + require(match is not None, f"{path.name}: unexpected source archive name") + version = match.group(1) + prefix = f"cuphoton-{version}/" + with tarfile.open(path, "r:gz") as archive: + files = { + member.name.removeprefix(prefix) + for member in archive + if member.isfile() + } + required = SOURCES | LICENSES | {"PKG-INFO"} + require( + required <= files, + f"{path.name}: missing {sorted(required - files)}", + ) + binaries = sorted(name for name in files if BINARY.search(name)) + require( + not binaries, + f"{path.name}: source archive contains binaries: {binaries}", + ) + check_metadata( + archive.extractfile(prefix + "PKG-INFO").read(), + version, + path.name, + "Apache-2.0", + ) + return version + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("directory", type=Path) + parser.add_argument("--arch", choices=ARCHITECTURES, action="append") + parser.add_argument("--version", help="Required release version") + args = parser.parse_args() + try: + require( + args.directory.is_dir(), + f"Not a distribution directory: {args.directory}", + ) + expected = { + (abi, arch) + for abi in ABIS + for arch in (args.arch or ARCHITECTURES) + } + found, versions = set(), set() + for path in sorted(args.directory.glob("*.whl")): + key, version = check_wheel(path) + require( + key not in found, f"Duplicate wheel for {key}: {path.name}" + ) + found.add(key) + versions.add(version) + require( + found == expected, + f"Wheel matrix mismatch: missing {sorted(expected - found)}, " + f"unexpected {sorted(found - expected)}", + ) + sdists = list(args.directory.glob("*.tar.gz")) + require( + len(sdists) == 1, + f"Expected one source archive, found {len(sdists)}", + ) + versions.add(check_sdist(sdists[0])) + require( + len(versions) == 1, + f"Inconsistent artifact versions: {sorted(versions)}", + ) + version = versions.pop() + require( + args.version is None or args.version == version, + f"Expected version {args.version}, found {version}", + ) + except ( + ValueError, + OSError, + KeyError, + zipfile.BadZipFile, + tarfile.TarError, + ) as error: + parser.exit(1, f"Distribution check failed: {error}\n") + print( + f"Validated {len(found)} native wheels and one source archive " + f"for cuPhoton {version}" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/wheels/install_build_dependencies.sh b/scripts/wheels/install_build_dependencies.sh new file mode 100755 index 00000000..def57790 --- /dev/null +++ b/scripts/wheels/install_build_dependencies.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# cibuildwheel puts the current build interpreter on PATH. +set -euo pipefail + +wheel_script_dir=$(cd "$(dirname "$0")" && pwd) +uv pip install --python "$(command -v python)" --only-binary :all: \ + --requirement "$wheel_script_dir/build-requirements.txt" diff --git a/scripts/wheels/prepare_cfitsio.sh b/scripts/wheels/prepare_cfitsio.sh new file mode 100755 index 00000000..66814fce --- /dev/null +++ b/scripts/wheels/prepare_cfitsio.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +# Run once per cibuildwheel Linux container, before the Python builds. +set -euo pipefail + +cfitsio_prefix=${CUPHOTON_XDR_CFITSIO_ROOT:?Set the CFITSIO installation prefix} +cfitsio_version=4.7.0 +cfitsio_sha256=ce573bbea8e75b429f8c3d3e86498741ba3dc9628a1530d2f65268397ad059e8 +cfitsio_build_dir=$(mktemp -d) +trap 'rm -rf "$cfitsio_build_dir"' EXIT + +cd "$cfitsio_build_dir" +curl --fail --location --retry 3 \ + --output cfitsio.tar.gz \ + "https://heasarc.gsfc.nasa.gov/FTP/software/fitsio/c/cfitsio-${cfitsio_version}.tar.gz" +printf '%s cfitsio.tar.gz\n' "$cfitsio_sha256" | sha256sum --check --strict +tar -xzf cfitsio.tar.gz +cd "cfitsio-${cfitsio_version}" +./configure --prefix="$cfitsio_prefix" \ + --enable-reentrant --disable-curl --without-bzip2 \ + --disable-static --enable-shared +make -j"${CUPHOTON_WHEEL_BUILD_JOBS:-2}" +make check +make install +install -Dm644 licenses/License.txt \ + "$cfitsio_prefix/share/licenses/cfitsio/License.txt" diff --git a/scripts/wheels/release.py b/scripts/wheels/release.py new file mode 100755 index 00000000..56ed66df --- /dev/null +++ b/scripts/wheels/release.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Resolve release tags and verify unchanged artifacts before publication.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import subprocess +from pathlib import Path +from urllib.error import HTTPError +from urllib.request import urlopen + +NUMBER = r"(?:0|[1-9][0-9]*)" +TAG = re.compile(rf"v({NUMBER}\.{NUMBER}\.{NUMBER}(?:rc{NUMBER})?)") + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def release_version(tag): + match = TAG.fullmatch(tag) + require(match is not None, f"Expected vX.Y.Z or vX.Y.ZrcN: {tag!r}") + return match[1] + + +def git(*args): + return subprocess.check_output(["git", *args], text=True).strip() + + +def resolve(tag, expected_sha=""): + version = release_version(tag) + sha = git("rev-parse", f"refs/tags/{tag}^{{commit}}") + require(not expected_sha or sha == expected_sha, "Release tag moved") + branches = ("refs/remotes/origin/main", "refs/remotes/origin/0.1.x") + require( + any( + subprocess.run( + ["git", "merge-base", "--is-ancestor", sha, branch], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ).returncode + == 0 + for branch in branches + ), + "Release commit must belong to main or 0.1.x", + ) + return version, sha + + +def hashes(directory): + paths = sorted(directory.iterdir()) + require(bool(paths), "No distributions found") + require( + all( + path.is_file() + and not path.is_symlink() + and path.name.endswith((".whl", ".tar.gz")) + for path in paths + ), + "Distribution directory contains unexpected files", + ) + result = {} + for path in paths: + with path.open("rb") as stream: + result[path.name] = hashlib.file_digest( + stream, "sha256" + ).hexdigest() + return result + + +def record(directory): + from check_distributions import check_sdist + + (sdist,) = directory.glob("*.tar.gz") + version = check_sdist(sdist) + requested = os.environ.get("RELEASE_VERSION", "") + require(not requested or version == requested, "Build version mismatch") + return { + "version": version, + "tag": f"v{requested}" if requested else None, + "source_sha": git("rev-parse", "HEAD"), + "repository": os.environ["GITHUB_REPOSITORY"], + "run_id": int(os.environ["GITHUB_RUN_ID"]), + "run_attempt": int(os.environ["GITHUB_RUN_ATTEMPT"]), + "workflow_sha": os.environ["GITHUB_WORKFLOW_SHA"], + "files": hashes(directory), + } + + +def validate_provenance(manifest, run, jobs, *, tag, sha, repository): + require(manifest["version"] == release_version(tag), "Wrong version") + require(manifest["tag"] == tag, "Wrong source tag") + require(manifest["source_sha"] == sha, "Wrong source commit") + require(manifest["repository"] == repository, "Wrong build repository") + require(manifest["run_id"] == run["id"], "Wrong build run") + require( + run["head_repository"]["full_name"] == repository, + "Build run belongs to another repository", + ) + require( + run["path"] == ".github/workflows/publish.yml", + "Expected a release workflow build", + ) + require( + manifest["workflow_sha"] == run["head_sha"], + "Wrong workflow commit", + ) + if run["event"] == "push": + require(run["head_branch"] == tag, "Run was for another tag") + require(run["head_sha"] == sha, "Tag run built another commit") + else: + require( + run["event"] == "workflow_dispatch" + and run["head_branch"] == "main", + "Expected tag push or manual release from main", + ) + require( + any( + job["name"] == "release wheels / distributions" + and job["status"] == "completed" + and job["conclusion"] == "success" + for job in jobs + ), + "Distribution validation did not succeed in the build attempt", + ) + + +def gh(endpoint): + return json.loads(subprocess.check_output(["gh", "api", endpoint])) + + +def verify(directory, manifest_path, tag, sha, run_id): + manifest = json.loads(manifest_path.read_text()) + repository = os.environ["GITHUB_REPOSITORY"] + require(str(manifest["run_id"]) == run_id, "Wrong artifact run") + require( + isinstance(manifest["run_attempt"], int) + and manifest["run_attempt"] > 0, + "Invalid build attempt", + ) + endpoint = f"repos/{repository}/actions/runs/{run_id}" + run = gh(endpoint) + jobs = gh( + f"{endpoint}/attempts/{manifest['run_attempt']}/jobs?per_page=100" + ) + require(jobs["total_count"] <= 100, "Unexpected release job count") + validate_provenance( + manifest, run, jobs["jobs"], tag=tag, sha=sha, repository=repository + ) + require(manifest["files"] == hashes(directory), "Artifact hashes differ") + + +def remove_published(directory, published): + """Retry partial uploads only when existing filenames match bytes.""" + local = hashes(directory) + remote = { + item["filename"]: item["digests"]["sha256"] for item in published + } + require( + all( + name in local and local[name] == sha + for name, sha in remote.items() + ), + "Index already contains different files for this version", + ) + for name in remote: + (directory / name).unlink() + return len(local) - len(remote) + + +def pending(directory, version, target): + release_version(f"v{version}") + host = "test.pypi.org" if target == "testpypi" else "pypi.org" + try: + with urlopen( + f"https://{host}/pypi/cuphoton/{version}/json", timeout=30 + ) as response: + published = json.load(response)["urls"] + except HTTPError as error: + if error.code != 404: + raise + published = [] + return remove_published(directory, published) + + +def output(**values): + with Path(os.environ["GITHUB_OUTPUT"]).open("a") as stream: + for key, value in values.items(): + stream.write(f"{key}={value}\n") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + select = commands.add_parser("resolve") + select.add_argument("tag") + select.add_argument("--expected-sha", default="") + save = commands.add_parser("record") + save.add_argument("directory", type=Path) + save.add_argument("manifest", type=Path) + check = commands.add_parser("verify") + check.add_argument("directory", type=Path) + check.add_argument("manifest", type=Path) + check.add_argument("--tag", required=True) + check.add_argument("--sha", required=True) + check.add_argument("--run-id", required=True) + upload = commands.add_parser("pending") + upload.add_argument("directory", type=Path) + upload.add_argument("--version", required=True) + upload.add_argument( + "--target", choices=("pypi", "testpypi"), required=True + ) + args = parser.parse_args() + if args.command == "resolve": + version, sha = resolve(args.tag, args.expected_sha) + output(version=version, sha=sha, tag=args.tag) + elif args.command == "record": + args.manifest.write_text( + json.dumps(record(args.directory), indent=2) + "\n" + ) + elif args.command == "verify": + verify(args.directory, args.manifest, args.tag, args.sha, args.run_id) + else: + output(pending=pending(args.directory, args.version, args.target)) + + +if __name__ == "__main__": + main() diff --git a/scripts/wheels/repair_wheel.py b/scripts/wheels/repair_wheel.py new file mode 100755 index 00000000..dd84ec35 --- /dev/null +++ b/scripts/wheels/repair_wheel.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Bundle CFITSIO and retain the declared CUDA/RAPIDS wheel dependencies.""" + +from __future__ import annotations + +import argparse +import importlib.util +import os +import platform +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + +# These libraries are supplied by the I/O extra, not copied into cuPhoton. +EXTERNAL_LIBRARIES = ( + "libcudart.so.13", + "libnvcomp.so.5", + "libkvikio.so", + "librapids_logger.so", +) + + +def library_directories() -> list[str]: + roots = [Path(os.environ["CUPHOTON_XDR_CFITSIO_ROOT"])] + for name in ( + "nvidia.cu13", + "nvidia.libnvcomp", + "libkvikio", + "rapids_logger", + ): + spec = importlib.util.find_spec(name) + if spec is None or not spec.submodule_search_locations: + raise RuntimeError(f"Missing native build package: {name}") + roots.extend(Path(path) for path in spec.submodule_search_locations) + return [ + str(directory) + for root in roots + for directory in (root / "lib64", root / "lib") + if directory.is_dir() + ] + + +def include_bundled_license(wheel: Path) -> None: + """Update the repaired artifact's license and regenerate its RECORD.""" + wheel = wheel.resolve() + with tempfile.TemporaryDirectory( + prefix=".cuphoton-license-", dir=wheel.parent + ) as temporary: + subprocess.run( + [ + sys.executable, + "-m", + "wheel", + "unpack", + "-d", + temporary, + str(wheel), + ], + check=True, + ) + metadata_files = list(Path(temporary).glob("*/*.dist-info/METADATA")) + if len(metadata_files) != 1: + raise RuntimeError("Expected one wheel METADATA file") + metadata = metadata_files[0] + content = metadata.read_bytes() + original = b"License-Expression: Apache-2.0" + if content.partition(b"\n\n")[0].splitlines().count(original) != 1: + raise RuntimeError( + "Expected Apache-2.0 license before bundling CFITSIO" + ) + metadata.write_bytes( + content.replace(original, original + b" AND CFITSIO", 1) + ) + subprocess.run( + [ + sys.executable, + "-m", + "wheel", + "pack", + "-d", + temporary, + str(metadata.parent.parent), + ], + check=True, + ) + (Path(temporary) / wheel.name).replace(wheel) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("wheel", type=Path) + parser.add_argument("destination", type=Path) + args = parser.parse_args() + architecture = platform.machine() + if architecture not in {"x86_64", "aarch64"}: + parser.error(f"Unsupported wheel architecture: {architecture}") + + environment = os.environ.copy() + directories = library_directories() + if environment.get("LD_LIBRARY_PATH"): + directories.append(environment["LD_LIBRARY_PATH"]) + environment["LD_LIBRARY_PATH"] = os.pathsep.join(directories) + command = [ + sys.executable, + "-m", + "auditwheel", + "repair", + "--plat", + f"manylinux_2_28_{architecture}", + "--wheel-dir", + str(args.destination), + ] + for library in EXTERNAL_LIBRARIES: + command.extend(("--exclude", library)) + command.append(str(args.wheel)) + subprocess.run(command, check=True, env=environment) + output_prefix = args.wheel.name.rsplit("-", 1)[0] + repaired = list(args.destination.glob(f"{output_prefix}-*.whl")) + if len(repaired) != 1: + raise RuntimeError(f"Expected one repaired wheel, found: {repaired}") + with zipfile.ZipFile(repaired[0]) as archive: + bundled = [ + name + for name in archive.namelist() + if ".libs/" in name and ".so" in Path(name).name + ] + if len(bundled) != 1 or not Path(bundled[0]).name.startswith( + "libcfitsio-" + ): + raise RuntimeError( + "Expected only a private CFITSIO library in the repaired wheel, " + f"found: {bundled}" + ) + include_bundled_license(repaired[0]) + + +if __name__ == "__main__": + main() diff --git a/scripts/wheels/test_installed.py b/scripts/wheels/test_installed.py new file mode 100644 index 00000000..3cf20319 --- /dev/null +++ b/scripts/wheels/test_installed.py @@ -0,0 +1,396 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Check a native wheel from an isolated Python outside the source tree.""" + +from __future__ import annotations + +import argparse +import ctypes +import faulthandler +import gc +import gzip +import hashlib +import importlib +import json +import os +import platform +import signal +import subprocess +import sys +import tempfile +import traceback +from concurrent.futures import ThreadPoolExecutor +from importlib import metadata +from pathlib import Path + +GPU_DISTRIBUTIONS = ( + "cupy-cuda13x", + "kvikio-cu13", + "libkvikio-cu13", + "nvidia-libnvcomp-cu13", + "nvidia-nvcomp-cu13", + "cuda-toolkit", + "nvidia-cuda-runtime", + "nvidia-cuda-nvrtc", + "nvidia-cufile", + "nvidia-nvjitlink", + "numba", + "numba-cuda", + "torch", +) +HDU_INDICES = (5, 1, 3, 2, 4) + + +def require(condition, message): + if not condition: + raise RuntimeError(message) + + +def passed(report, name): + report["checks"].append(name) + print(f"PASS {name}", file=sys.stderr, flush=True) + + +def module_path(distribution, name): + module = importlib.import_module(name) + path = Path(module.__file__).resolve() + installed = { + Path(distribution.locate_file(item)).resolve() + for item in distribution.files or () + } + require(path in installed, f"{name} is outside wheel RECORD: {path}") + return path + + +def loaded_native_libraries(): + prefixes = ( + "libcudart", + "libcufile", + "libcfitsio", + "libnvcomp", + "libkvikio", + ) + paths = set() + for line in Path("/proc/self/maps").read_text().splitlines(): + fields = line.split(maxsplit=5) + if len(fields) == 6 and Path(fields[5]).name.startswith(prefixes): + paths.add(fields[5]) + return sorted(paths) + + +def check_install(mode, report): + require(sys.flags.isolated, "Run this script with python -I") + distribution = metadata.distribution("cuphoton") + wheel = distribution.read_text("WHEEL") or "" + require("Root-Is-Purelib: false" in wheel, "Expected a native wheel") + direct_url = json.loads(distribution.read_text("direct_url.json") or "{}") + require( + not direct_url.get("dir_info", {}).get("editable", False), + "Editable installations cannot qualify a wheel", + ) + report["wheel_metadata"] = wheel + report["versions"] = {"cuphoton": distribution.version} + report["paths"] = { + name: str(module_path(distribution, name)) + for name in ("cuphoton", "cuphoton.xdr") + } + for name in GPU_DISTRIBUTIONS: + try: + report["versions"][name] = metadata.version(name) + except metadata.PackageNotFoundError: + continue + require(mode != "base", f"Base environment includes {name}") + if mode == "base": + require( + not any( + name == prefix or name.startswith(prefix + ".") + for name in sys.modules + for prefix in ("cupy", "kvikio", "nvidia.nvcomp") + ), + "Base imports loaded an optional GPU package", + ) + for argv in (["--version"], ["xdr", "--help"]): + result = subprocess.run( + [ + sys.executable, + "-I", + "-c", + "from cuphoton.core.cli import main; " + f"raise SystemExit(main({argv!r}))", + ], + capture_output=True, + text=True, + timeout=30, + check=True, + ) + require(bool(result.stdout.strip()), f"Empty CLI output for {argv}") + if argv == ["--version"]: + require( + result.stdout.strip() == distribution.version, + "CLI and installed metadata versions differ", + ) + passed(report, "installed_origin_and_base_imports") + return distribution + + +def create_fits(directory): + import numpy as np + from astropy.io import fits + + paths = [] + grid = np.arange(17 * 23, dtype=np.int32).reshape(17, 23) - 200 + for index in range(6): + integers = grid + index * 1000 + floats = integers.astype(np.float32) / 4 + hdus = [ + fits.PrimaryHDU(), + fits.ImageHDU(integers.astype(np.int16)), + ] + for data in (integers, floats): + for compression in ("GZIP_1", "GZIP_2"): + hdus.append( + fits.CompImageHDU( + data=data, + compression_type=compression, + tile_shape=(6, 7), + quantize_level=0, + ) + ) + path = directory / f"image-{index}.fits" + fits.HDUList(hdus).writeto(path) + paths.append(path) + return [paths[index] for index in (4, 0, 5, 2, 1, 3)] + + +def reference_images(paths, hdu_indices=HDU_INDICES, section=None): + import numpy as np + from astropy.io import fits + + outputs = [[] for _ in hdu_indices] + for path in paths: + with fits.open(path, memmap=False) as hdus: + for output, hdu_index in zip(outputs, hdu_indices, strict=True): + data = hdus[hdu_index].data + if section is not None: + data = data[section] + output.append(data.astype(data.dtype.newbyteorder("="))) + return tuple(np.stack(output) for output in outputs) + + +def check_native(distribution, paths, report): + from cuphoton.xdr.nvcomp_batch import ( + cpp_helper_available, + get_native_batch_builder, + get_native_plan_files, + ) + + require(cpp_helper_available(), "Native extension cannot be loaded") + get_native_batch_builder(required=True) + planner = get_native_plan_files(required=True) + extension = module_path(distribution, "cuphoton.xdr._nvcomp_batch_ext") + report["paths"]["native_extension"] = str(extension) + report["extension_sha256"] = hashlib.sha256( + extension.read_bytes() + ).hexdigest() + report["loaded_native_libraries"] = loaded_native_libraries() + plans = planner( + [str(path) for path in paths], + list(range(len(paths))), + list(HDU_INDICES), + 2, + None, + ) + require(len(plans) == len(paths), "Native planner lost files") + require( + [plan[1] for plan in plans] == list(range(len(paths))), + "Native planner changed file order", + ) + passed(report, "native_extension_builder_and_cfitsio_planner") + + +def check_images(actual, expected): + import cupy as cp + import numpy as np + + require(len(actual) == len(expected), "Output HDU count differs") + for output, reference in zip(actual, expected, strict=True): + require(isinstance(output, cp.ndarray), "Output is not on the GPU") + require(output.shape == reference.shape, "Output shape differs") + require(output.dtype == reference.dtype, "Output dtype differs") + np.testing.assert_array_equal(cp.asnumpy(output), reference) + + +def check_gpu(paths, report): + import cupy as cp + import kvikio.defaults + import numpy as np + + from cuphoton.xdr import batch_to_device, batch_to_device_stream + from cuphoton.xdr.nvcomp_batch import ( + gpu_gzip_decompress_batch, + native_device_pool_stats, + ) + + require(cp.cuda.runtime.getDeviceCount() > 0, "A CUDA GPU is required") + properties = cp.cuda.runtime.getDeviceProperties(cp.cuda.Device().id) + report["gpu"] = { + "name": properties["name"].decode(), + "compute_capability": [properties["major"], properties["minor"]], + } + require( + kvikio.defaults.is_compat_mode_preferred(), + "Compatibility I/O was not enabled", + ) + report["io_mode"] = "kvikio_compat" + report["cupy_runtime_version"] = cp.cuda.runtime.runtimeGetVersion() + report["cuda_driver"] = cp.cuda.runtime.driverGetVersion() + runtime_path = next( + path + for path in report["loaded_native_libraries"] + if Path(path).name.startswith("libcudart") + ) + runtime = ctypes.CDLL(runtime_path) + runtime.cudaRuntimeGetVersion.argtypes = [ctypes.POINTER(ctypes.c_int)] + runtime.cudaRuntimeGetVersion.restype = ctypes.c_int + version = ctypes.c_int() + require( + runtime.cudaRuntimeGetVersion(ctypes.byref(version)) == 0, + "Loaded CUDA runtime version query failed", + ) + report["loaded_cuda_runtime_version"] = version.value + raw = bytes(range(256)) * 4 + compressed = gzip.compress(raw, mtime=0) + encoded = cp.asarray(np.frombuffer(compressed, dtype=np.uint8)) + decoded, offsets = gpu_gzip_decompress_batch( + encoded, [0], [len(compressed)], [len(raw)], use_cpp_helper=True + ) + np.testing.assert_array_equal( + cp.asnumpy(decoded), np.frombuffer(raw, "u1") + ) + np.testing.assert_array_equal(offsets, [0]) + passed(report, "forced_cpp_gzip_decompression") + expected = reference_images(paths) + options = dict( + hdu_indices=HDU_INDICES, + native_batcher=True, + native_read_threads=2, + native_plan_threads=2, + decode_batch_files=2, + batch_queue_depth=1, + prefetch_depth=2, + ) + retained = batch_to_device(paths, **options) + check_images(retained, expected) + passed(report, "native_image_gzip1_gzip2_order_and_dtype") + + stream = cp.cuda.Stream(non_blocking=True) + supplied = tuple( + cp.empty(array.shape, dtype=array.dtype) for array in expected + ) + actual = batch_to_device_stream( + paths, out=supplied, stream=stream, **options + ) + require( + all( + output.data.ptr == buffer.data.ptr + for output, buffer in zip(actual, supplied, strict=True) + ), + "Caller-provided output buffers were replaced", + ) + check_images(actual, expected) + gc.collect() + check_images(retained, expected) + passed(report, "nondefault_stream_outputs_and_retained_arrays") + + section = (slice(2, 16), slice(3, 22)) + roi_options = dict(options, hdu_indices=(5, 2, 3, 4), section=section) + roi_expected = reference_images( + paths, roi_options["hdu_indices"], section + ) + check_images(batch_to_device_stream(paths, **roi_options), roi_expected) + passed(report, "compressed_edge_tiles_and_roi") + + fallback = dict(options, native_batcher=False) + check_images(batch_to_device_stream(paths, **fallback), expected) + passed(report, "python_prefetch_with_native_planning") + + device_id = cp.cuda.Device().id + + def load_concurrently(): + with cp.cuda.Device(device_id): + own_stream = cp.cuda.Stream(non_blocking=True) + arrays = batch_to_device_stream( + paths, stream=own_stream, **options + ) + check_images(arrays, expected) + + with ThreadPoolExecutor(max_workers=2) as executor: + futures = [executor.submit(load_concurrently) for _ in range(2)] + for future in futures: + future.result() + check_images(retained, expected) + passed(report, "two_concurrent_native_readers") + del actual, supplied, retained + cp.cuda.runtime.deviceSynchronize() + gc.collect() + stats = native_device_pool_stats(device_id) + require(stats is not None, "Native device pool statistics unavailable") + require(stats["checked_out_bytes"] == 0, "Native device buffers leaked") + report["native_device_pool"] = stats + passed(report, "native_buffer_release") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--mode", choices=("base", "native", "gpu"), required=True + ) + parser.add_argument("--output", type=Path) + parser.add_argument("--timeout", type=int, default=300) + args = parser.parse_args() + require(args.timeout > 0, "--timeout must be positive") + output = args.output.resolve() if args.output else None + report = { + "mode": args.mode, + "python": sys.version, + "architecture": platform.machine(), + "checks": [], + "ok": False, + } + faulthandler.dump_traceback_later(args.timeout, exit=True) + faulthandler.register(signal.SIGUSR1, all_threads=True) + try: + if args.mode == "gpu": + os.environ["KVIKIO_COMPAT_MODE"] = "ON" + os.environ["KVIKIO_NTHREADS"] = "2" + with tempfile.TemporaryDirectory(prefix="cuphoton-wheel-") as scratch: + os.chdir(scratch) + if args.mode == "gpu": + os.environ["CUPY_CACHE_DIR"] = str( + Path(scratch) / "cupy-cache" + ) + distribution = check_install(args.mode, report) + if args.mode != "base": + paths = create_fits(Path(scratch)) + report["versions"]["astropy"] = metadata.version("astropy") + check_native(distribution, paths, report) + if args.mode == "gpu": + check_gpu(paths, report) + report["ok"] = True + except Exception as exc: + report["error"] = f"{type(exc).__name__}: {exc}" + traceback.print_exc() + finally: + faulthandler.cancel_dump_traceback_later() + payload = json.dumps(report, indent=2, sort_keys=True) + "\n" + if output is not None: + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(payload) + print(payload, end="") + return 0 if report["ok"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/wheels/test_stack.py b/scripts/wheels/test_stack.py new file mode 100644 index 00000000..44ae6500 --- /dev/null +++ b/scripts/wheels/test_stack.py @@ -0,0 +1,420 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Fail-closed single-node wheel checks for optional compute runtimes. + +Run with python -I, using mpiexec or dragon for the corresponding mode. +CPU runtime checks execute xPois in real workers; --backend cupy exercises +the GPU-only product executors. Run test_installed.py --mode gpu separately +to qualify native XDR, CFITSIO, KvikIO, and nvCOMP on the same wheel. +""" + +from __future__ import annotations + +import argparse +import importlib +import json +import os +import platform +import shutil +import sys +import tempfile +import traceback +from importlib import metadata +from pathlib import Path + +RUNTIMES = { + "cutile": ("cuda-tile", "cuda.tile"), + "mpi": ("mpi4py", "mpi4py.MPI"), + "dragon": ("dragonhpc", "dragon"), +} + + +def require(condition, message): + if not condition: + raise RuntimeError(message) + + +def check_install(report): + require(sys.flags.isolated, "Run this script with python -I") + distribution = metadata.distribution("cuphoton") + require( + "Root-Is-Purelib: false" in (distribution.read_text("WHEEL") or ""), + "Expected a native wheel", + ) + direct = json.loads(distribution.read_text("direct_url.json") or "{}") + require( + not direct.get("dir_info", {}).get("editable", False), + "Editable installations cannot qualify a wheel", + ) + recorded = { + Path(distribution.locate_file(item)).resolve() + for item in distribution.files or () + } + for name in ("cuphoton", "cuphoton.xpois"): + path = Path(importlib.import_module(name).__file__).resolve() + require(path in recorded, f"{name} is outside wheel RECORD: {path}") + report["versions"] = {"cuphoton": distribution.version} + report["checks"].append("installed_wheel_origin") + + +def check_imports(names, report): + require(bool(names), "--expect must select at least one runtime") + for name in names: + require(name in RUNTIMES, f"Unknown runtime: {name}") + distribution, module = RUNTIMES[name] + importlib.import_module(module) + report["versions"][distribution] = metadata.version(distribution) + report["checks"].append(f"import_{module}") + + +def fixture(worker_id=0): + import numpy as np + from scipy.signal import fftconvolve + + from cuphoton.xpois import ( + GaussianBasisComponent, + build_gaussian_polynomial_basis, + ) + + reference = np.random.default_rng(913 + worker_id).normal(size=(48, 53)) + components = [GaussianBasisComponent(sigma=1.5, degree=0)] + basis, _ = build_gaussian_polynomial_basis((9, 9), components) + scale = 1.7 + worker_id / 10 + target = fftconvolve(reference, scale * basis[0], mode="same") + 0.2 + return reference, target, components, scale + + +def solve(backend, worker_id=0): + import numpy as np + + from cuphoton.xpois import solve_constant_kernel + + reference, target, components, scale = fixture(worker_id) + result = solve_constant_kernel( + reference, + target, + components, + kernel_shape=(9, 9), + variance=np.ones_like(target), + background_degree=0, + backend=backend, + ) + require(result.backend == backend, "Requested backend was not used") + np.testing.assert_allclose(result.kernel_coefficients, [scale], atol=1e-8) + np.testing.assert_allclose( + result.background_coefficients, [0.2], atol=1e-8 + ) + np.testing.assert_allclose(result.residual[result.fit_mask], 0, atol=1e-8) + return { + "worker_id": worker_id, + "pid": os.getpid(), + "hostname": platform.node(), + "backend": result.backend, + "scale": float(result.kernel_coefficients[0]), + "fit_pixel_count": result.fit_pixel_count, + } + + +def check_workers(results, count): + import numpy as np + + require(len(results) == count, "Missing worker results") + ordered = sorted(results, key=lambda item: item["worker_id"]) + require( + [item["worker_id"] for item in ordered] == list(range(count)), + "Missing or duplicate worker identities", + ) + require( + len({(item["hostname"], item["pid"]) for item in ordered}) == count, + "Workers did not execute in distinct processes", + ) + np.testing.assert_allclose( + [item["scale"] for item in ordered], + [1.7 + worker / 10 for worker in range(count)], + atol=1e-8, + ) + + +def check_gpu(report): + import cupy as cp + import torch + + from cuphoton.xscan.training import cupy_to_torch, predict_tensors + + require(cp.cuda.runtime.getDeviceCount() > 0, "A CUDA GPU is required") + require(torch.cuda.is_available(), "Torch CUDA is required") + for distribution in ("cupy-cuda13x", "cuda-tile", "numba-cuda", "torch"): + report["versions"][distribution] = metadata.version(distribution) + report["gpu"] = { + "name": torch.cuda.get_device_name(0), + "compute_capability": list(torch.cuda.get_device_capability(0)), + "cuda_visible_devices": os.environ.get("CUDA_VISIBLE_DEVICES"), + } + for backend in ("cpu", "cupy", "numba-cuda", "cutile"): + report[backend] = solve(backend) + report["checks"].append(f"xpois_{backend}_known_solution") + + device = torch.device("cuda:0") + values = ( + cp.arange(2 * 2 * 9 * 9, dtype=cp.float32).reshape(2, 2, 9, 9) / 100 + ) + view = cupy_to_torch(values, device=device) + require(view.tensor.data_ptr() == values.data.ptr, "DLPack copied data") + linear = torch.nn.Linear(2 * 9 * 9, 1).to(device) + with torch.no_grad(): + linear.weight.fill_(1 / (2 * 9 * 9)) + linear.bias.zero_() + model = torch.nn.Sequential( + torch.nn.Flatten(), linear, torch.nn.Flatten(0, 1) + ) + result = predict_tensors(model=model, images=view.tensor, device=device) + torch.testing.assert_close( + result["logits"], view.tensor.mean(dim=(1, 2, 3)) + ) + torch.testing.assert_close( + result["probabilities"], result["logits"].sigmoid() + ) + torch.cuda.synchronize() + report["checks"].append("xscan_cupy_dlpack_torch_cuda_inference") + + +def create_manifest(directory, count): + import numpy as np + + pairs = [] + for worker_id in range(count): + reference, target, _, _ = fixture(worker_id) + pair = {"id": f"pair-{worker_id}"} + for name, data in (("reference", reference), ("target", target)): + path = directory / f"{name}-{worker_id}.npy" + np.save(path, data) + pair[name] = str(path) + pairs.append(pair) + path = directory / "pairs.json" + path.write_text( + json.dumps( + {"schema": "cuphoton.xpois.image-pairs/v1", "pairs": pairs} + ) + ) + return path + + +def check_product_result(result, workers, report): + import numpy as np + + from cuphoton.xpois import build_gaussian_polynomial_basis + + require(result is not None, "Coordinator did not return a result") + report["executor_summary"] = result.summary + require(result.status == "success", "Product executor failed") + summary = result.summary + if summary["executor"] == "dragon": + require(len(summary["placements"]) == workers, "Too few GPU workers") + for worker_id in range(workers): + artifacts = ( + result.run_dir / "items" / f"pair-{worker_id}" / "artifacts" + ) + residual = np.load(artifacts / "residual.npy") + np.testing.assert_allclose(residual[4:-4, 4:-4], 0, atol=1e-8) + _, _, components, scale = fixture(worker_id) + basis, _ = build_gaussian_polynomial_basis((9, 9), components) + np.testing.assert_allclose( + np.load(artifacts / "kernel.npy"), scale * basis[0], atol=1e-8 + ) + report["checks"].append(f"xpois_{summary['executor']}_gpu_executor") + + +def options(): + from cuphoton.xpois.batch import BatchFitOptions + + return BatchFitOptions( + kernel_shape=(9, 9), + basis_sigmas=(1.5,), + basis_degrees=(0,), + backend="cupy", + ) + + +def check_mpi(args, report): + import numpy as np + from mpi4py import MPI + + comm = MPI.COMM_WORLD + require( + comm.size == args.workers, "MPI world size differs from --workers" + ) + report["versions"]["mpi4py"] = metadata.version("mpi4py") + report["mpi_library"] = MPI.Get_library_version() + if args.backend == "cpu": + result = solve("cpu", comm.rank) + total = comm.allreduce(result["scale"], op=MPI.SUM) + np.testing.assert_allclose( + total, + sum(1.7 + rank / 10 for rank in range(comm.size)), + atol=1e-8, + ) + results = comm.gather(result, root=0) + if comm.rank == 0: + check_workers(results, args.workers) + report["workers"] = results + report["checks"].append("mpi_launched_xpois_cpu_collectives") + else: + from cuphoton.xpois.mpi import run_mpi_image_pair_batch + + directory = comm.bcast( + tempfile.mkdtemp(prefix="cuphoton-stack-") + if comm.rank == 0 + else None, + root=0, + ) + root = Path(directory) + if comm.rank == 0: + create_manifest(root, args.workers) + comm.Barrier() + result = run_mpi_image_pair_batch( + manifest_path=root / "pairs.json", + output_root=root / "output", + run_id="acceptance", + aggregation_mode="mpi", + rank_timeout_sec=None, + attempt_id=None, + options=options(), + rank_setup_timeout_sec=120, + ) + if comm.rank == 0: + check_product_result(result, args.workers, report) + shutil.rmtree(root) + return comm.rank == 0 + + +def check_dragon(args, report): + from dragon.native.process import ProcessTemplate + from dragon.native.process_group import ProcessGroup + + report["versions"]["dragonhpc"] = metadata.version("dragonhpc") + with tempfile.TemporaryDirectory(prefix="cuphoton-stack-") as directory: + root = Path(directory) + if args.backend == "cupy": + from cuphoton.xpois.dragon import run_dragon_image_pair_batch + + result = run_dragon_image_pair_batch( + manifest_path=create_manifest(root, args.workers), + output_root=root / "output", + run_id="acceptance", + max_workers=args.workers, + result_timeout_sec=30, + worker_timeout_sec=120, + options=options(), + ) + check_product_result(result, args.workers, report) + return + group = ProcessGroup(restart=False) + try: + for worker_id in range(args.workers): + group.add_process( + nproc=1, + template=ProcessTemplate( + target=sys.executable, + args=( + "-I", + str(Path(__file__).resolve()), + "--mode", + "worker", + "--worker-id", + str(worker_id), + "--report", + str(root / f"worker-{worker_id}.json"), + ), + cwd=directory, + ), + ) + group.init() + group.start() + group.join(timeout=120) + statuses = list(group.inactive_puids) + require( + len(statuses) == args.workers, "Missing Dragon exit status" + ) + require( + all(code == 0 for _, code in statuses), "Dragon worker failed" + ) + results = [ + json.loads((root / f"worker-{worker_id}.json").read_text())[ + "result" + ] + for worker_id in range(args.workers) + ] + check_workers(results, args.workers) + report["workers"] = results + report["checks"].append("dragon_launched_xpois_cpu_workers") + finally: + group.close(patience=5) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--mode", + choices=("imports", "gpu", "mpi", "dragon", "worker"), + required=True, + ) + parser.add_argument("--backend", choices=("cpu", "cupy"), default="cpu") + parser.add_argument("--workers", type=int, default=2) + parser.add_argument("--worker-id", type=int, default=0) + parser.add_argument("--expect", default="") + parser.add_argument("--report", type=Path, required=True) + args = parser.parse_args(argv) + report = { + "schema": "cuphoton.wheel-stack-acceptance/v1", + "mode": args.mode, + "backend": args.backend if args.mode in {"mpi", "dragon"} else None, + "requested_workers": ( + args.workers if args.mode in {"mpi", "dragon"} else None + ), + "python": sys.version, + "platform": platform.platform(), + "checks": [], + "status": "failed", + } + writer = True + try: + require(args.workers > 0, "--workers must be positive") + check_install(report) + if args.mode == "imports": + check_imports( + args.expect.split(",") if args.expect else [], report + ) + elif args.mode == "gpu": + check_gpu(report) + elif args.mode == "mpi": + writer = check_mpi(args, report) + elif args.mode == "dragon": + check_dragon(args, report) + else: + report["result"] = solve("cpu", args.worker_id) + report["status"] = "passed" + return 0 + except Exception: + report["error"] = traceback.format_exc() + print(report["error"], file=sys.stderr, flush=True) + return 1 + finally: + # Rank-specific failure receipts avoid races if a collective fails. + if args.mode == "mpi" and "mpi4py.MPI" in sys.modules: + from mpi4py import MPI + + if report["status"] != "passed": + args.report = args.report.with_name( + f"{args.report.stem}.rank-{MPI.COMM_WORLD.rank}.json" + ) + else: + writer = MPI.COMM_WORLD.rank == 0 + if writer: + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(json.dumps(report, indent=2) + "\n") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/cuphoton/__init__.py b/src/cuphoton/__init__.py index 0ed831b7..337f79c1 100644 --- a/src/cuphoton/__init__.py +++ b/src/cuphoton/__init__.py @@ -4,6 +4,10 @@ """GPU-accelerated astronomy and imaging tools from NVIDIA.""" -__version__ = "0.1.3" +try: + from ._version import __version__ +except ModuleNotFoundError: + # An unbuilt source checkout has no resolved distribution version yet. + __version__ = "0.0.0.dev0" __all__ = ["__version__"] diff --git a/src/cuphoton/_photometry.py b/src/cuphoton/_photometry.py index 9609843a..1299299c 100644 --- a/src/cuphoton/_photometry.py +++ b/src/cuphoton/_photometry.py @@ -329,7 +329,7 @@ def _load_background_api(): from photutils.background import Background2D, MedianBackground except ImportError as exc: raise RuntimeError( - "Photometry requires the `photutils` package." + "Photometry requires `photutils`; install cuphoton[photometry]." ) from exc return Background2D, MedianBackground @@ -341,7 +341,8 @@ def _load_segmentation_api(): from photutils.utils.exceptions import NoDetectionsWarning except ImportError as exc: raise RuntimeError( - "Source detection requires the `photutils` package." + "Source detection requires `photutils`; " + "install cuphoton[photometry]." ) from exc return photutils_detect, SourceCatalog, NoDetectionsWarning @@ -354,6 +355,7 @@ def _load_aperture_api(): ) except ImportError as exc: raise RuntimeError( - "Aperture photometry requires the `photutils` package." + "Aperture photometry requires `photutils`; " + "install cuphoton[photometry]." ) from exc return EllipticalAperture, aperture_photometry diff --git a/src/cuphoton/core/_mpi_runtime.py b/src/cuphoton/core/_mpi_runtime.py index 9ae882ba..b345561f 100644 --- a/src/cuphoton/core/_mpi_runtime.py +++ b/src/cuphoton/core/_mpi_runtime.py @@ -313,8 +313,11 @@ def _load_mpi_api() -> _MPIAPI: from mpi4py import MPI except (ImportError, OSError, RuntimeError) as exc: raise RuntimeError( - "MPI collective aggregation requires mpi4py built against the " - "allocation's MPI runtime" + "MPI collective aggregation requires mpi4py and a compatible " + "MPI runtime; install the Python bindings with " + "pip install 'cuphoton[mpi]', provide the allocation's MPI " + "runtime, and launch with its matching mpiexec/mpirun or " + "scheduler launcher" ) from exc try: library_version = _normalize_mpi_library_version( diff --git a/src/cuphoton/core/dragon.py b/src/cuphoton/core/dragon.py index 39ed70ac..806b4d5c 100644 --- a/src/cuphoton/core/dragon.py +++ b/src/cuphoton/core/dragon.py @@ -169,8 +169,9 @@ def _load_dragon_api() -> _DragonAPI: except (ImportError, OSError) as exc: raise RuntimeError( "The Dragon executor requires the dragonhpc runtime; " - "install it in this Python environment on every node " - "and launch with dragon" + "install it with pip install 'cuphoton[dragon]' in a Python " + "3.12 or 3.13 environment on every node and launch with dragon. " + "The supported Dragon release has no Python 3.14 wheels" ) from exc return _DragonAPI( System=System, diff --git a/src/cuphoton/xdr/gds.py b/src/cuphoton/xdr/gds.py index 9a4b7cf5..e5dc7e05 100644 --- a/src/cuphoton/xdr/gds.py +++ b/src/cuphoton/xdr/gds.py @@ -22,6 +22,7 @@ from typing import Sequence _KVIKIO_DEFAULTS_LOCK = threading.Lock() +_KVIKIO_NUM_THREADS: int | None = None def available_cpu_cores() -> int: @@ -36,16 +37,24 @@ def available_cpu_cores() -> int: def configure_kvikio_parallelism() -> int: - """Configure KvikIO's default thread pool for parallel FITS reads.""" - import kvikio.defaults as defaults + """Initialize the shared KvikIO pool before the first cuPhoton reader. - num_threads = available_cpu_cores() - # Setting even the current size resets the pool and waits for active I/O. - # Keep concurrent callers from acting on a stale pool size. + Keep an explicit KVIKIO_NTHREADS setting; otherwise use available CPUs. + Resetting this global pool while another reader uses it is unsafe. + """ + global _KVIKIO_NUM_THREADS with _KVIKIO_DEFAULTS_LOCK: - if defaults.get("num_threads") != num_threads: - defaults.set("num_threads", num_threads) - return num_threads + if _KVIKIO_NUM_THREADS is None: + import kvikio.defaults as defaults + + num_threads = int(defaults.get("num_threads")) + if "KVIKIO_NTHREADS" not in os.environ: + desired_threads = available_cpu_cores() + if num_threads != desired_threads: + defaults.set("num_threads", desired_threads) + num_threads = int(defaults.get("num_threads")) + _KVIKIO_NUM_THREADS = num_threads + return _KVIKIO_NUM_THREADS @contextmanager diff --git a/src/cuphoton/xdr/nvcomp_batch.py b/src/cuphoton/xdr/nvcomp_batch.py index 11b8eb08..3eed7c35 100644 --- a/src/cuphoton/xdr/nvcomp_batch.py +++ b/src/cuphoton/xdr/nvcomp_batch.py @@ -44,7 +44,10 @@ def _load_shared_library(path: Path | str) -> None: def _package_dir(module_name: str) -> Path | None: - spec = importlib.util.find_spec(module_name) + try: + spec = importlib.util.find_spec(module_name) + except ModuleNotFoundError: + return None if spec is None: return None locations = spec.submodule_search_locations @@ -110,6 +113,11 @@ def _candidate_cuda_homes(): if path not in seen: seen.add(path) yield path + for module_name in ("nvidia.cu13", "nvidia.cuda_runtime"): + path = _package_dir(module_name) + if path is not None and path not in seen: + seen.add(path) + yield path default = Path("/usr/local/cuda") if default not in seen: yield default @@ -120,25 +128,9 @@ def _preload_cudart() -> None: for cuda_home in _candidate_cuda_homes(): for lib_name in ("lib64", "lib"): lib_dir = cuda_home / lib_name - candidates.extend( - [ - lib_dir / "libcudart.so.13", - lib_dir / "libcudart.so", - ] - ) - - cuda_runtime_base = _package_dir("nvidia.cuda_runtime") - if cuda_runtime_base is not None: - runtime_lib = cuda_runtime_base / "lib" - candidates.extend( - [ - runtime_lib / "libcudart.so.13", - runtime_lib / "libcudart.so", - ] - ) + candidates.append(lib_dir / "libcudart.so.13") candidates.append("libcudart.so.13") - candidates.append("libcudart.so") last_error: OSError | None = None for candidate in candidates: @@ -152,9 +144,10 @@ def _preload_cudart() -> None: detail = f" Last loader error was: {last_error}" if last_error else "" raise ImportError( - "Could not preload libcudart for " + "Could not preload libcudart.so.13 for " "cuphoton.xdr._nvcomp_batch_ext. " - "Set CUDA_HOME to a CUDA toolkit root containing libcudart." + detail + "Install cuphoton[io] or set CUDA_HOME to a CUDA 13 toolkit root." + + detail ) @@ -269,7 +262,8 @@ def get_native_batch_builder(required: bool = False): msg = ( "native_batcher=True but " "`_nvcomp_batch_ext.NativeBatchBuilder` is " - "not importable. Build it from a source checkout with " + "not importable. Install `cuphoton[io]` from a native wheel, " + "or build it from a source checkout with " "`bash src/cuphoton/xdr/src/build.sh` (see " "docs/components/xdr.md); the native builder requires KvikIO." ) @@ -296,7 +290,8 @@ def get_native_plan_files(required: bool = False): msg = ( "native_batcher=True but " "`_nvcomp_batch_ext.plan_native_files` is " - "not importable. Build it from a source checkout with " + "not importable. Install `cuphoton[io]` from a native wheel, " + "or build it from a source checkout with " "`bash src/cuphoton/xdr/src/build.sh` (see " "docs/components/xdr.md); the native planner requires CFITSIO." ) @@ -444,7 +439,8 @@ def _warn_python_fallback_once() -> None: "Python `nvcomp.as_array` fallback path, which is ~6x slower " "than the " "optional C++ helper (`_nvcomp_batch_ext`). " - "To enable the fast path, build the native extension: " + "To enable the fast path, install `cuphoton[io]` from a native wheel " + "or build the native extension from source: " "`bash src/cuphoton/xdr/src/build.sh`." ) if _CPP_EXT_IMPORT_ERROR: @@ -829,7 +825,8 @@ def gpu_gzip_decompress_batch( raise RuntimeError( "use_cpp_helper=True but `_nvcomp_batch_ext` is not " "importable. " - "Run `bash src/cuphoton/xdr/src/build.sh` first. " + "Install `cuphoton[io]` from a native wheel or run " + "`bash src/cuphoton/xdr/src/build.sh` from source. " f"Import error: {_CPP_EXT_IMPORT_ERROR}" ) elif n == 0: diff --git a/src/cuphoton/xdr/setup_package.py b/src/cuphoton/xdr/setup_package.py index d4143522..65701983 100644 --- a/src/cuphoton/xdr/setup_package.py +++ b/src/cuphoton/xdr/setup_package.py @@ -37,7 +37,10 @@ def _require_dir(path: Path, description: str) -> Path: def _package_dir(module_name: str) -> Path: - spec = importlib.util.find_spec(module_name) + try: + spec = importlib.util.find_spec(module_name) + except ModuleNotFoundError: + spec = None if spec is None: raise RuntimeError( f"Required Python package is not importable: {module_name}" @@ -102,6 +105,14 @@ def _cuda_home_candidates(): if path not in seen: seen.add(path) yield path + for module_name in ("nvidia.cu13", "nvidia.cuda_runtime"): + try: + path = _package_dir(module_name) + except RuntimeError: + continue + if path not in seen: + seen.add(path) + yield path default = Path("/usr/local/cuda") if default not in seen: seen.add(default) @@ -116,24 +127,21 @@ def _cuda_home_candidates(): def _find_cuda_toolkit(): for cuda_home in _cuda_home_candidates(): include_dir = cuda_home / "include" - if not (include_dir / "cuda_runtime.h").is_file(): + if not all( + (include_dir / header).is_file() + for header in ("cuda_runtime.h", "crt/host_config.h") + ): continue for lib_name in ("lib64", "lib"): lib_dir = cuda_home / lib_name - if any( - (lib_dir / soname).is_file() - for soname in ( - "libcudart.so", - "libcudart.so.13", - ) - ): + if (lib_dir / "libcudart.so.13").is_file(): return include_dir, lib_dir raise RuntimeError( - "CUDA toolkit with cuda_runtime.h and libcudart is required to build " - "cuphoton.xdr._nvcomp_batch_ext. Set CUDA_HOME to the " - "toolkit root." + "CUDA 13 headers (including CRT) and libcudart.so.13 are required " + "to build cuphoton.xdr._nvcomp_batch_ext. Install the CUDA 13 " + "development wheels or set CUDA_HOME to the toolkit root." ) @@ -177,12 +185,13 @@ def _find_cufile_include(cuda_include: Path) -> Path: if (cuda_include / "cufile.h").is_file(): return cuda_include - try: - candidate = _package_dir("nvidia.cufile") / "include" + for module_name in ("nvidia.cu13", "nvidia.cufile"): + try: + candidate = _package_dir(module_name) / "include" + except RuntimeError: + continue if (candidate / "cufile.h").is_file(): return candidate - except RuntimeError: - pass raise RuntimeError( "cuFile header cufile.h is required to build " @@ -322,7 +331,7 @@ def get_extensions(): "-pthread", "-l:libnvcomp.so.5", "-l:libkvikio.so", - "-lcudart", + "-l:libcudart.so.13", *cfitsio_paths["extra_link_args"], ], ) diff --git a/src/cuphoton/xdr/src/build.sh b/src/cuphoton/xdr/src/build.sh index a02c79f2..efbb703e 100755 --- a/src/cuphoton/xdr/src/build.sh +++ b/src/cuphoton/xdr/src/build.sh @@ -38,7 +38,9 @@ fi cd "$ROOT_DIR" printf 'Building xdr with Python: %s\n' "$PYTHON" +uv pip install --python "$PYTHON" \ + 'setuptools>=83.0.0' 'setuptools-scm==10.3.4' wheel 'pybind11>=3.0,<4' exec env CUPHOTON_XDR_BUILD_EXT=1 uv pip install \ --python "$PYTHON" \ --no-build-isolation \ - -e "$ROOT_DIR[gpu]" + -e "$ROOT_DIR[io]" diff --git a/tests/test_package_layout.py b/tests/test_package_layout.py index 492ef86c..02c05011 100644 --- a/tests/test_package_layout.py +++ b/tests/test_package_layout.py @@ -11,6 +11,7 @@ from pathlib import Path import pytest +from packaging.requirements import Requirement import cuphoton @@ -57,7 +58,7 @@ def test_distribution_metadata_declares_supported_profiles() -> None: distribution = metadata.distribution("cuphoton") assert set(distribution.metadata["Requires-Python"].split(",")) == { "<3.15", - ">=3.11", + ">=3.12", } assert "Programming Language :: Python :: 3.14" in ( distribution.metadata.get_all("Classifier") or () @@ -65,21 +66,43 @@ def test_distribution_metadata_declares_supported_profiles() -> None: assert set(distribution.metadata.get_all("Provides-Extra") or ()) == { "cutile", "dev", + "dragon", "gpu", + "io", + "mpi", + "photometry", "torch", "viz", } - requirements = distribution.metadata.get_all("Requires-Dist") or () - cutile_requirements = [ - requirement - for requirement in requirements - if 'extra == "cutile"' in requirement + + +@pytest.mark.parametrize("python_version", ("3.12", "3.13", "3.14")) +def test_optional_runtime_requirements_are_not_silently_omitted( + python_version: str, +) -> None: + requirements = [ + Requirement(value) for value in metadata.requires("cuphoton") or () ] - assert len(cutile_requirements) == 2 - assert all( - 'python_version < "3.14"' in requirement - for requirement in cutile_requirements - ) + + def selected(extra: str) -> dict[str, Requirement]: + environment = { + "extra": extra, + "python_version": python_version, + "platform_system": "Linux", + } + return { + requirement.name: requirement + for requirement in requirements + if requirement.marker and requirement.marker.evaluate(environment) + } + + cutile = selected("cutile") + assert {"cuda-tile", "cupy-cuda13x"} <= cutile.keys() + assert "mpi4py" in selected("mpi") + # Dragon has no 3.14 wheel yet: its extra must fail resolution there, + # rather than appear to install successfully without the runtime. + assert "dragonhpc" in selected("dragon") + assert not {"mpi4py", "dragonhpc", "cuda-tile"} & selected("gpu").keys() def test_distribution_exposes_only_the_umbrella_console_script() -> None: @@ -134,6 +157,7 @@ def test_cli_help_and_version_do_not_import_optional_gpu_packages( "kvikio", "mpi4py", "numba", + "photutils", "torch", ) diff --git a/tests/test_release.py b/tests/test_release.py new file mode 100644 index 00000000..b9575578 --- /dev/null +++ b/tests/test_release.py @@ -0,0 +1,378 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Exercise release identity and artifact promotion without publishing.""" + +from __future__ import annotations + +import hashlib +import importlib.util +import io +import json +import subprocess +import tarfile +from pathlib import Path +from types import SimpleNamespace +from urllib.error import HTTPError + +import pytest + +SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" / "wheels" +SPEC = importlib.util.spec_from_file_location( + "release", SCRIPTS / "release.py" +) +release = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(release) + + +@pytest.mark.parametrize( + "version", ["0.1.3", "1.2.30", "0.1.3rc0", "0.1.3rc12"] +) +def test_canonical_release_tags(version): + assert release.release_version(f"v{version}") == version + + +@pytest.mark.parametrize( + "tag", + [ + "0.1.3", + "v0.1", + "v00.1.3", + "v0.01.3", + "v0.1.03", + "v0.1.3rc01", + "v0.1.3RC1", + "v0.1.3.dev1", + "v0.1.3+local", + "v0.1.3\n", + "v0.1.3; echo bad", + "--help", + ], +) +def test_noncanonical_release_tags_are_rejected(tag): + with pytest.raises(ValueError, match="Expected vX.Y.Z"): + release.release_version(tag) + + +@pytest.fixture +def source(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + + def git(*args): + return subprocess.check_output( + ["git", *args], text=True, stderr=subprocess.PIPE + ).strip() + + git("init", "--initial-branch=main") + git("config", "user.name", "Release Tests") + git("config", "user.email", "tests@nvidia.com") + git("config", "commit.gpgsign", "false") + git("config", "tag.gpgsign", "false") + git("commit", "--allow-empty", "-m", "Initial source") + sha = git("rev-parse", "HEAD") + git("update-ref", "refs/remotes/origin/main", sha) + return git, sha + + +def test_tags_resolve_to_commits_and_detect_a_moved_tag(source): + git, sha = source + git("tag", "-a", "v0.1.3rc0", "-m", "Release candidate") + git("tag", "v0.1.3") + assert release.resolve("v0.1.3rc0", sha) == ("0.1.3rc0", sha) + assert release.resolve("v0.1.3", sha) == ("0.1.3", sha) + git("commit", "--allow-empty", "-m", "Later source") + git("update-ref", "refs/remotes/origin/main", "HEAD") + # A reviewed ancestor remains valid after the release branch advances. + assert release.resolve("v0.1.3", sha) == ("0.1.3", sha) + git("tag", "--force", "v0.1.3") + with pytest.raises(ValueError, match="Release tag moved"): + release.resolve("v0.1.3", sha) + + +def test_tag_requires_a_release_branch_ancestor(source): + git, _ = source + git("commit", "--allow-empty", "-m", "Unreviewed source") + sha = git("rev-parse", "HEAD") + git("tag", "v0.1.4") + with pytest.raises(ValueError, match="must belong to main or 0.1.x"): + release.resolve("v0.1.4") + git("update-ref", "refs/remotes/origin/0.1.x", sha) + assert release.resolve("v0.1.4") == ("0.1.4", sha) + + +@pytest.fixture +def build(tmp_path, monkeypatch): + dist = tmp_path / "dist" + dist.mkdir() + for filename in ("cuphoton-0.1.3.tar.gz", "cuphoton-0.1.3-example.whl"): + (dist / filename).write_bytes(filename.encode()) + source_sha, workflow_sha = "a" * 40, "b" * 40 + manifest = { + "version": "0.1.3", + "tag": "v0.1.3", + "source_sha": source_sha, + "repository": "NVIDIA/cuPhoton", + "run_id": 123, + "run_attempt": 1, + "workflow_sha": workflow_sha, + "files": release.hashes(dist), + } + run = { + "id": 123, + "head_repository": {"full_name": "NVIDIA/cuPhoton"}, + "path": ".github/workflows/publish.yml", + "head_sha": workflow_sha, + "event": "workflow_dispatch", + "head_branch": "main", + "run_attempt": 2, + "status": "in_progress", + "conclusion": None, + } + jobs = [ + { + "name": "release wheels / distributions", + "status": "completed", + "conclusion": "success", + } + ] + monkeypatch.setenv("GITHUB_REPOSITORY", "NVIDIA/cuPhoton") + return SimpleNamespace( + dist=dist, + manifest=manifest, + run=run, + jobs=jobs, + sha=source_sha, + manifest_path=tmp_path / "provenance.json", + ) + + +def validate(build): + release.validate_provenance( + build.manifest, + build.run, + build.jobs, + tag="v0.1.3", + sha=build.sha, + repository="NVIDIA/cuPhoton", + ) + + +def test_manual_promotion_uses_original_build_attempt(build, monkeypatch): + # The workflow runs on main while building an older tagged source SHA. + # Later publication can still be pending or failed in the parent run. + endpoints = [] + + def gh(endpoint): + endpoints.append(endpoint) + if endpoint.endswith("/jobs?per_page=100"): + return {"total_count": 1, "jobs": build.jobs} + return build.run + + monkeypatch.setattr(release, "gh", gh) + build.manifest_path.write_text(json.dumps(build.manifest)) + release.verify( + build.dist, build.manifest_path, "v0.1.3", build.sha, "123" + ) + assert endpoints == [ + "repos/NVIDIA/cuPhoton/actions/runs/123", + "repos/NVIDIA/cuPhoton/actions/runs/123/attempts/1/jobs?per_page=100", + ] + build.run.update(status="completed", conclusion="failure") + validate(build) + + +@pytest.mark.parametrize( + "section,key,value,error", + [ + ("manifest", "version", "0.1.3rc0", "Wrong version"), + ("manifest", "tag", "v0.1.3rc0", "Wrong source tag"), + ("manifest", "source_sha", "b" * 40, "Wrong source commit"), + ("manifest", "workflow_sha", "a" * 40, "Wrong workflow commit"), + ( + "manifest", + "repository", + "elsewhere/project", + "Wrong build repository", + ), + ("manifest", "run_id", 456, "Wrong build run"), + ("run", "path", ".github/workflows/ci.yml", "release workflow"), + ("run", "event", "pull_request", "manual release from main"), + ("run", "head_branch", "topic", "manual release from main"), + ( + "run", + "head_repository", + {"full_name": "fork/cuPhoton"}, + "another repository", + ), + ], +) +def test_provenance_mismatch_is_rejected(build, section, key, value, error): + getattr(build, section)[key] = value + with pytest.raises(ValueError, match=error): + validate(build) + + +def test_push_requires_the_exact_tag_and_commit(build): + build.run.update(event="push", head_branch="v0.1.3", head_sha=build.sha) + build.manifest["workflow_sha"] = build.sha + validate(build) + build.run["head_branch"] = "v0.1.3rc0" + with pytest.raises(ValueError, match="another tag"): + validate(build) + build.run["head_branch"] = "v0.1.3" + build.run["head_sha"] = build.manifest["workflow_sha"] = "c" * 40 + with pytest.raises(ValueError, match="another commit"): + validate(build) + + +@pytest.mark.parametrize( + "status,conclusion", + [ + ("in_progress", None), + ("queued", None), + ("completed", "failure"), + ("completed", "cancelled"), + ("completed", "skipped"), + ], +) +def test_incomplete_or_failed_build_cannot_be_promoted( + build, status, conclusion +): + build.jobs[0].update(status=status, conclusion=conclusion) + with pytest.raises( + ValueError, match="Distribution validation did not succeed" + ): + validate(build) + + +def test_another_successful_job_is_not_distribution_validation(build): + build.jobs[0]["name"] = "stage" + with pytest.raises( + ValueError, match="Distribution validation did not succeed" + ): + validate(build) + + +def test_changed_artifact_bytes_cannot_be_promoted(build, monkeypatch): + monkeypatch.setattr( + release, + "gh", + lambda endpoint: ( + {"total_count": 1, "jobs": build.jobs} + if "/attempts/" in endpoint + else build.run + ), + ) + build.manifest_path.write_text(json.dumps(build.manifest)) + next(build.dist.glob("*.whl")).write_bytes(b"changed after qualification") + with pytest.raises(ValueError, match="Artifact hashes differ"): + release.verify( + build.dist, build.manifest_path, "v0.1.3", build.sha, "123" + ) + + +def test_provenance_sidecar_must_stay_outside_distributions(build): + (build.dist / "provenance.json").write_text("{}") + with pytest.raises(ValueError, match="unexpected files"): + release.hashes(build.dist) + + +@pytest.mark.parametrize("published_count", [0, 1, 2]) +def test_retry_removes_only_byte_identical_published_files( + build, published_count +): + files = list(build.manifest["files"].items()) + published = [ + {"filename": name, "digests": {"sha256": digest}} + for name, digest in files[:published_count] + ] + assert ( + release.remove_published(build.dist, published) == 2 - published_count + ) + assert {path.name for path in build.dist.iterdir()} == { + name for name, _ in files[published_count:] + } + + +@pytest.mark.parametrize("unknown_filename", [False, True]) +def test_retry_hash_collision_keeps_every_local_file(build, unknown_filename): + before = release.hashes(build.dist) + (first, digest), (second, _) = before.items() + published = [ + {"filename": first, "digests": {"sha256": digest}}, + { + "filename": "unknown.whl" if unknown_filename else second, + "digests": {"sha256": "0" * 64}, + }, + ] + with pytest.raises(ValueError, match="different files"): + release.remove_published(build.dist, published) + assert release.hashes(build.dist) == before + + +@pytest.mark.parametrize( + "target,host", [("pypi", "pypi.org"), ("testpypi", "test.pypi.org")] +) +def test_new_index_version_keeps_all_files(build, monkeypatch, target, host): + def missing(url, *, timeout): + assert url == f"https://{host}/pypi/cuphoton/0.1.3/json" + assert timeout > 0 + raise HTTPError(url, 404, "Not found", {}, None) + + monkeypatch.setattr(release, "urlopen", missing) + assert release.pending(build.dist, "0.1.3", target) == 2 + + +def test_index_failure_is_not_treated_as_an_unpublished_version( + build, monkeypatch +): + def failed(url, *, timeout): + raise HTTPError(url, 503, "Unavailable", {}, None) + + monkeypatch.setattr(release, "urlopen", failed) + with pytest.raises(HTTPError): + release.pending(build.dist, "0.1.3", "pypi") + assert release.hashes(build.dist) == build.manifest["files"] + + +def test_record_keeps_tagged_source_and_workflow_identity_separate( + source, tmp_path, monkeypatch +): + monkeypatch.syspath_prepend(str(SCRIPTS)) + from check_distributions import LICENSES, SOURCES + + _, sha = source + dist = tmp_path / "dist" + dist.mkdir() + sdist = dist / "cuphoton-0.1.3.tar.gz" + metadata = ( + "Name: cuphoton\nVersion: 0.1.3\nLicense-Expression: Apache-2.0\n" + "Requires-Python: >=3.12,<3.15\n" + "Provides-Extra: io\nProvides-Extra: gpu\n" + ).encode() + with tarfile.open(sdist, "w:gz") as archive: + for name in sorted(SOURCES | LICENSES | {"PKG-INFO"}): + content = metadata if name == "PKG-INFO" else b"source\n" + member = tarfile.TarInfo(f"cuphoton-0.1.3/{name}") + member.size = len(content) + archive.addfile(member, io.BytesIO(content)) + for key, value in { + "RELEASE_VERSION": "0.1.3", + "GITHUB_REPOSITORY": "NVIDIA/cuPhoton", + "GITHUB_RUN_ID": "123", + "GITHUB_RUN_ATTEMPT": "2", + "GITHUB_WORKFLOW_SHA": "b" * 40, + }.items(): + monkeypatch.setenv(key, value) + manifest = release.record(dist) + assert manifest["source_sha"] == sha + assert manifest["workflow_sha"] == "b" * 40 + assert manifest["tag"] == "v0.1.3" + assert manifest["run_attempt"] == 2 + assert manifest["files"] == { + sdist.name: hashlib.sha256(sdist.read_bytes()).hexdigest() + } + monkeypatch.setenv("RELEASE_VERSION", "0.1.3rc0") + with pytest.raises(ValueError, match="Build version mismatch"): + release.record(dist) diff --git a/tests/test_versioning.py b/tests/test_versioning.py new file mode 100644 index 00000000..8ce332b1 --- /dev/null +++ b/tests/test_versioning.py @@ -0,0 +1,221 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Exercise the real version configuration in small, isolated source trees.""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tarfile +import tomllib +import zipfile +from email.parser import Parser +from pathlib import Path + +import pytest +from packaging.version import Version + +ROOT = Path(__file__).resolve().parents[1] + + +def _run(*args: str, cwd: Path, env: dict[str, str] | None = None): + return subprocess.run( + args, + cwd=cwd, + env=env, + check=True, + text=True, + capture_output=True, + ) + + +@pytest.fixture +def source(tmp_path: Path) -> Path: + source = tmp_path / "source" + package = source / "src" / "cuphoton" + package.mkdir(parents=True) + shutil.copyfile( + ROOT / "src/cuphoton/__init__.py", package / "__init__.py" + ) + config = tomllib.loads((ROOT / "pyproject.toml").read_text()) + lines = [ + "[build-system]", + f"requires = {json.dumps(config['build-system']['requires'])}", + 'build-backend = "setuptools.build_meta"', + "[project]", + 'name = "cuphoton"', + 'dynamic = ["version"]', + "[tool.setuptools.packages.find]", + 'where = ["src"]', + "[tool.setuptools_scm]", + ] + lines.extend( + f"{key} = {json.dumps(value)}" + for key, value in config["tool"]["setuptools_scm"].items() + ) + (source / "pyproject.toml").write_text("\n".join(lines) + "\n") + for name in (".gitignore", "MANIFEST.in"): + shutil.copyfile(ROOT / name, source / name) + for name in ( + "AGENTS.md", + "CHANGELOG.md", + "CLAUDE.md", + "RELEASING.md", + ".github/workflows/example.yml", + "scripts/excluded.py", + "examples/excluded.ipynb", + ): + path = source / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("excluded source-only file\n") + _run("git", "init", cwd=source) + _run("git", "config", "user.name", "Packaging Tests", cwd=source) + _run("git", "config", "user.email", "tests@example.org", cwd=source) + _run("git", "config", "commit.gpgsign", "false", cwd=source) + _run("git", "config", "tag.gpgsign", "false", cwd=source) + _run("git", "add", ".", cwd=source) + _run("git", "commit", "-m", "Initial source", cwd=source) + return source + + +def _build( + source: Path, + output: Path, + kind: str, + *, + override: str | None = None, +) -> Path: + env = { + key: value + for key, value in os.environ.items() + if not key.startswith("SETUPTOOLS_SCM_") + } + if override is not None: + env["SETUPTOOLS_SCM_PRETEND_VERSION_FOR_CUPHOTON"] = override + _run( + "uv", + "build", + f"--{kind}", + "--out-dir", + str(output), + str(source), + cwd=source, + env=env, + ) + suffix = "*.whl" if kind == "wheel" else "*.tar.gz" + (artifact,) = output.glob(suffix) + return artifact + + +def _wheel_version(wheel: Path, output: Path) -> str: + with zipfile.ZipFile(wheel) as archive: + (metadata_path,) = ( + name for name in archive.namelist() if name.endswith("/METADATA") + ) + version = Parser().parsestr(archive.read(metadata_path).decode())[ + "Version" + ] + archive.extractall(output) + result = _run( + sys.executable, + "-I", + "-c", + "import sys; sys.path.insert(0, sys.argv[1]); " + "import cuphoton; from importlib.metadata import version; " + "assert cuphoton.__version__ == version('cuphoton'); " + "print(cuphoton.__version__)", + str(output), + cwd=output, + ) + assert result.stdout.strip() == version + return version + + +@pytest.mark.parametrize("version", ["0.1.3rc0", "0.1.3"]) +def test_tag_version_survives_gitless_sdist( + source: Path, tmp_path: Path, version: str +) -> None: + _run("git", "tag", f"v{version}", cwd=source) + sdist = _build(source, tmp_path / "sdist", "sdist") + unpacked = tmp_path / "unpacked" + with tarfile.open(sdist) as archive: + names = [Path(name).parts[1:] for name in archive.getnames()] + assert ("src", "cuphoton", "_version.py") in names + assert not any( + parts + and ( + parts[0] == ".github" + or parts[-1] + in {"AGENTS.md", "CHANGELOG.md", "CLAUDE.md", "RELEASING.md"} + or parts[-1].endswith(".ipynb") + or parts == ("scripts", "excluded.py") + ) + for parts in names + ) + archive.extractall(unpacked, filter="data") + (gitless_source,) = unpacked.iterdir() + assert not (gitless_source / ".git").exists() + wheel = _build(gitless_source, tmp_path / "wheel", "wheel") + assert _wheel_version(wheel, tmp_path / "installed") == version + + +def test_event_version_selects_rc_or_final_on_same_commit( + source: Path, tmp_path: Path +) -> None: + _run("git", "tag", "v0.1.3rc0", cwd=source) + _run("git", "tag", "v0.1.3", cwd=source) + head = _run("git", "rev-parse", "HEAD", cwd=source).stdout + for version in ("0.1.3rc0", "0.1.3"): + wheel = _build(source, tmp_path / version, "wheel", override=version) + assert _wheel_version(wheel, tmp_path / f"installed-{version}") == ( + version + ) + assert _run("git", "rev-parse", "HEAD", cwd=source).stdout == head + + +def test_untagged_descendant_is_next_development_version( + source: Path, tmp_path: Path +) -> None: + _run("git", "tag", "v0.1.2", cwd=source) + (source / "README.md").write_text("Development change\n") + _run("git", "add", "README.md", cwd=source) + _run("git", "commit", "-m", "Development change", cwd=source) + wheel = _build(source, tmp_path / "wheel", "wheel") + version = Version(_wheel_version(wheel, tmp_path / "installed")) + assert version.release == (0, 1, 3) + assert version.dev == 1 + assert version.local is not None + + +def test_unrecognized_release_tag_is_rejected( + source: Path, tmp_path: Path +) -> None: + _run("git", "tag", "v0.1.3beta0", cwd=source) + with pytest.raises(subprocess.CalledProcessError): + _build(source, tmp_path / "wheel", "wheel") + + +def test_unbuilt_source_has_development_fallback(source: Path) -> None: + result = _run( + sys.executable, + "-I", + "-c", + "import sys; sys.path.insert(0, sys.argv[1]); " + "import cuphoton; print(cuphoton.__version__)", + str(source / "src"), + cwd=source, + ) + assert result.stdout.strip() == "0.0.0.dev0" + + +def test_unversioned_archive_does_not_build_as_fallback( + source: Path, tmp_path: Path +) -> None: + shutil.rmtree(source / ".git") + with pytest.raises(subprocess.CalledProcessError): + _build(source, tmp_path / "wheel", "wheel") diff --git a/tests/test_wheel_stack.py b/tests/test_wheel_stack.py new file mode 100644 index 00000000..b54ea325 --- /dev/null +++ b/tests/test_wheel_stack.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Check that wheel acceptance rejects incomplete execution evidence.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest + +SCRIPT = Path(__file__).parents[1] / "scripts" / "wheels" / "test_stack.py" +SPEC = importlib.util.spec_from_file_location("wheel_stack", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +stack = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(stack) + + +@pytest.fixture +def workers(): + return [ + { + "worker_id": worker_id, + "hostname": "node", + "pid": 100 + worker_id, + "scale": 1.7 + worker_id / 10, + } + for worker_id in range(2) + ] + + +def test_worker_evidence_rejects_missing_worker(workers): + with pytest.raises(RuntimeError, match="Missing worker"): + stack.check_workers(workers[:1], 2) + + +def test_worker_evidence_rejects_duplicated_identity(workers): + workers[1]["worker_id"] = 0 + with pytest.raises(RuntimeError, match="duplicate worker identities"): + stack.check_workers(workers, 2) + + +def test_worker_evidence_rejects_reused_process(workers): + workers[1]["pid"] = workers[0]["pid"] + with pytest.raises(RuntimeError, match="distinct processes"): + stack.check_workers(workers, 2) + + +def test_worker_evidence_rejects_wrong_numerical_result(workers): + workers[1]["scale"] = 0 + with pytest.raises(AssertionError): + stack.check_workers(workers, 2) + + +def test_cpu_product_fixture_has_known_solution(): + result = stack.solve("cpu", worker_id=1) + assert result["backend"] == "cpu" + assert result["scale"] == pytest.approx(1.8, abs=1e-8) + assert result["fit_pixel_count"] == (48 - 8) * (53 - 8) diff --git a/tests/xdr/test_build_script.py b/tests/xdr/test_build_script.py index 2a6afafc..71fd2397 100644 --- a/tests/xdr/test_build_script.py +++ b/tests/xdr/test_build_script.py @@ -81,7 +81,7 @@ def test_build_selects_interpreter_and_preserves_install_arguments( str(expected), "--no-build-isolation", "-e", - f"{root}[gpu]", + f"{root}[io]", ] diff --git a/tests/xdr/test_cuda_discovery.py b/tests/xdr/test_cuda_discovery.py new file mode 100644 index 00000000..e9cf4f75 --- /dev/null +++ b/tests/xdr/test_cuda_discovery.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import sys +from pathlib import Path +from types import ModuleType + +import pytest + +from cuphoton.xdr import nvcomp_batch, setup_package + + +def _cuda_tree(root, *, library="libcudart.so.13"): + (root / "include" / "crt").mkdir(parents=True) + (root / "include" / "crt" / "host_config.h").touch() + (root / "include" / "cuda_runtime.h").touch() + (root / "lib").mkdir() + (root / "lib" / library).touch() + return root + + +@pytest.fixture +def cuda_wheel(monkeypatch, tmp_path): + # CUDA 13 wheels share a namespace directory without __init__.py files. + root = _cuda_tree(tmp_path / "site-packages" / "nvidia" / "cu13") + (root / "include" / "cufile.h").touch() + namespace = ModuleType("nvidia") + namespace.__path__ = [str(root.parent)] + monkeypatch.setitem(sys.modules, "nvidia", namespace) + for name in ("nvidia.cu13", "nvidia.cuda_runtime", "nvidia.cufile"): + monkeypatch.delitem(sys.modules, name, raising=False) + monkeypatch.delenv("CUDA_HOME", raising=False) + monkeypatch.delenv("CUDA_PATH", raising=False) + return root + + +def test_cuda_wheel_supplies_runtime_and_build_headers( + monkeypatch, tmp_path, cuda_wheel +): + system_toolkit = _cuda_tree(tmp_path / "system-cuda") + monkeypatch.setattr( + setup_package.shutil, + "which", + lambda name: str(system_toolkit / "bin" / "nvcc"), + ) + loaded = [] + monkeypatch.setattr(nvcomp_batch, "_load_shared_library", loaded.append) + + nvcomp_batch._preload_cudart() + headers, libraries = setup_package._find_cuda_toolkit() + + assert loaded == [cuda_wheel / "lib" / "libcudart.so.13"] + assert headers == cuda_wheel / "include" + assert libraries == cuda_wheel / "lib" + assert not (libraries / "libcudart.so").exists() + # A system toolkit can lack cuFile even when its wheel is installed. + assert ( + setup_package._find_cufile_include(system_toolkit / "include") + == cuda_wheel / "include" + ) + + +@pytest.mark.parametrize("variable", ["CUDA_HOME", "CUDA_PATH"]) +def test_explicit_cuda_toolkit_precedes_wheel( + monkeypatch, tmp_path, cuda_wheel, variable +): + toolkit = _cuda_tree(tmp_path / "selected-cuda") + monkeypatch.setenv(variable, str(toolkit)) + loaded = [] + monkeypatch.setattr(nvcomp_batch, "_load_shared_library", loaded.append) + + nvcomp_batch._preload_cudart() + + assert loaded == [toolkit / "lib" / "libcudart.so.13"] + assert setup_package._find_cuda_toolkit() == ( + toolkit / "include", + toolkit / "lib", + ) + + +@pytest.mark.parametrize("available", [True, False]) +def test_runtime_fallback_never_loads_unversioned_cuda( + monkeypatch, tmp_path, available +): + toolkit = _cuda_tree(tmp_path / "old-cuda", library="libcudart.so") + monkeypatch.setattr( + nvcomp_batch, "_candidate_cuda_homes", lambda: iter([toolkit]) + ) + loaded = [] + + def load_library(path): + loaded.append(path) + if not available: + raise OSError("CUDA 13 runtime is unavailable") + + monkeypatch.setattr(nvcomp_batch, "_load_shared_library", load_library) + + if available: + nvcomp_batch._preload_cudart() + else: + with pytest.raises( + ImportError, match="CUDA 13 runtime is unavailable" + ): + nvcomp_batch._preload_cudart() + + assert loaded == ["libcudart.so.13"] + + +def test_runtime_falls_back_after_unloadable_library(monkeypatch, tmp_path): + toolkit = _cuda_tree(tmp_path / "broken-cuda") + monkeypatch.setattr( + nvcomp_batch, "_candidate_cuda_homes", lambda: iter([toolkit]) + ) + loaded = [] + + def load_library(path): + loaded.append(path) + if isinstance(path, Path): + raise OSError("invalid ELF header") + + monkeypatch.setattr(nvcomp_batch, "_load_shared_library", load_library) + + nvcomp_batch._preload_cudart() + + assert loaded == [ + toolkit / "lib" / "libcudart.so.13", + "libcudart.so.13", + ] + + +def test_build_rejects_toolkit_without_cuda13_runtime(monkeypatch, tmp_path): + toolkit = _cuda_tree(tmp_path / "old-cuda", library="libcudart.so") + monkeypatch.setattr( + setup_package, "_cuda_home_candidates", lambda: iter([toolkit]) + ) + + with pytest.raises(RuntimeError, match="libcudart.so.13"): + setup_package._find_cuda_toolkit() + + +def test_cuda_package_lookup_handles_absent_nvidia_namespace(monkeypatch): + monkeypatch.setitem(sys.modules, "nvidia", None) + monkeypatch.delitem(sys.modules, "nvidia.cu13", raising=False) + + assert nvcomp_batch._package_dir("nvidia.cu13") is None + with pytest.raises(RuntimeError, match="not importable: nvidia.cu13"): + setup_package._package_dir("nvidia.cu13") + + +def test_build_skips_runtime_wheel_without_crt(monkeypatch, tmp_path): + runtime = _cuda_tree(tmp_path / "runtime-only") + (runtime / "include" / "crt" / "host_config.h").unlink() + toolkit = _cuda_tree(tmp_path / "complete-sdk") + monkeypatch.setattr( + setup_package, + "_cuda_home_candidates", + lambda: iter([runtime, toolkit]), + ) + + assert setup_package._find_cuda_toolkit() == ( + toolkit / "include", + toolkit / "lib", + ) diff --git a/tests/xdr/test_gds.py b/tests/xdr/test_gds.py new file mode 100644 index 00000000..277cd5a2 --- /dev/null +++ b/tests/xdr/test_gds.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import sys +import time +from concurrent.futures import ThreadPoolExecutor +from threading import Barrier +from types import ModuleType + +import pytest + +from cuphoton.xdr import gds + + +@pytest.fixture +def kvikio_defaults(monkeypatch): + kvikio = ModuleType("kvikio") + defaults = ModuleType("kvikio.defaults") + kvikio.defaults = defaults + monkeypatch.setitem(sys.modules, "kvikio", kvikio) + monkeypatch.setitem(sys.modules, "kvikio.defaults", defaults) + monkeypatch.setattr(gds, "_KVIKIO_NUM_THREADS", None) + monkeypatch.delenv("KVIKIO_NTHREADS", raising=False) + return defaults + + +def test_concurrent_readers_initialize_pool_once( + monkeypatch, kvikio_defaults +): + pool_threads = 1 + resets = [] + start = Barrier(8) + + def reset_pool(name, count): + resets.append((name, count)) + # The real C++ pool reset releases the GIL. Other readers must wait + # until it finishes, then use this pool without resetting it again. + time.sleep(0.01) + nonlocal pool_threads + pool_threads = count + + def configure_reader(): + start.wait(timeout=5) + return gds.configure_kvikio_parallelism() + + monkeypatch.setattr(gds, "available_cpu_cores", lambda: 12) + kvikio_defaults.set = reset_pool + kvikio_defaults.get = lambda name: pool_threads + with ThreadPoolExecutor(max_workers=8) as readers: + futures = [readers.submit(configure_reader) for _ in range(8)] + assert [future.result(timeout=5) for future in futures] == [12] * 8 + assert resets == [("num_threads", 12)] + + # Later readers cannot resize a pool that earlier readers still use. + monkeypatch.setattr(gds, "available_cpu_cores", lambda: 24) + assert gds.configure_kvikio_parallelism() == 12 + assert resets == [("num_threads", 12)] + + +def test_explicit_kvikio_threads_do_not_reset_pool( + monkeypatch, kvikio_defaults +): + monkeypatch.setenv("KVIKIO_NTHREADS", "2") + kvikio_defaults.get = lambda name: 2 + + def reject_reset(*args): + raise AssertionError("Explicit KvikIO configuration was overwritten") + + kvikio_defaults.set = reject_reset + monkeypatch.setattr(gds, "available_cpu_cores", reject_reset) + + assert gds.configure_kvikio_parallelism() == 2 + assert gds.configure_kvikio_parallelism() == 2 + + +def test_failed_pool_initialization_can_retry(monkeypatch, kvikio_defaults): + attempts = [] + + def reset_pool(name, count): + attempts.append(count) + if len(attempts) == 1: + raise RuntimeError("Pool initialization failed") + + monkeypatch.setattr(gds, "available_cpu_cores", lambda: 4) + kvikio_defaults.set = reset_pool + kvikio_defaults.get = lambda name: 4 if len(attempts) >= 2 else 1 + + with pytest.raises(RuntimeError, match="Pool initialization failed"): + gds.configure_kvikio_parallelism() + assert gds.configure_kvikio_parallelism() == 4 + assert attempts == [4, 4] diff --git a/tests/xdr/test_reader_concurrency.py b/tests/xdr/test_reader_concurrency.py index 6ac29c5c..f2c4d203 100644 --- a/tests/xdr/test_reader_concurrency.py +++ b/tests/xdr/test_reader_concurrency.py @@ -59,6 +59,8 @@ def close(self): "kvikio", SimpleNamespace(defaults=defaults, CuFile=CuFile), ) + monkeypatch.setattr(gds, "_KVIKIO_NUM_THREADS", None) + monkeypatch.delenv("KVIKIO_NTHREADS", raising=False) monkeypatch.setattr(gds, "available_cpu_cores", lambda: desired_threads) monkeypatch.setattr(storage_cache, "active", lambda: False) @@ -75,7 +77,8 @@ def close(self): desired_threads = 8 with gds.GdsHeapLoader("third.fits") as third: assert third._file().path == "third.fits" - assert resets == [4, 8] + # Later affinity changes must not reset a pool exposed to earlier readers. + assert resets == [4] @pytest.fixture @@ -572,6 +575,8 @@ def contend(): def test_concurrent_kvikio_configuration_does_not_reset_active_io( monkeypatch, ): + monkeypatch.setattr(gds, "_KVIKIO_NUM_THREADS", None) + monkeypatch.delenv("KVIKIO_NTHREADS", raising=False) configured_threads = 1 pending = False resets = [] diff --git a/tests/xpois/test_dragon.py b/tests/xpois/test_dragon.py index 9942c6ad..4c10c248 100644 --- a/tests/xpois/test_dragon.py +++ b/tests/xpois/test_dragon.py @@ -4,6 +4,7 @@ from __future__ import annotations +import builtins import json import os import queue @@ -110,6 +111,37 @@ def _valid_shard_result( } +@pytest.mark.parametrize( + "import_error", + [ + ModuleNotFoundError("No module named 'dragon'"), + OSError("Dragon shared library could not be loaded"), + ], +) +def test_dragon_import_failure_explains_installation( + monkeypatch: pytest.MonkeyPatch, import_error: Exception +) -> None: + original_import = builtins.__import__ + + def blocked_import(name, *args, **kwargs): + if name.startswith("dragon."): + raise import_error + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", blocked_import) + + with pytest.raises(RuntimeError) as error: + dragon_module._load_dragon_api() + + message = str(error.value) + assert "pip install 'cuphoton[dragon]'" in message + assert "Python 3.12 or 3.13" in message + assert "every node" in message + assert "launch with dragon" in message + assert "no Python 3.14 wheels" in message + assert error.value.__cause__ is import_error + + def test_discovery_uses_actual_noncontiguous_node_gpu_ids() -> None: assert discover_gpu_placements(_System, _Node) == ( Placement(worker_id=0, host="node-7", gpu_id=2), diff --git a/tests/xpois/test_mpi.py b/tests/xpois/test_mpi.py index 910f9cfe..6ecf6771 100644 --- a/tests/xpois/test_mpi.py +++ b/tests/xpois/test_mpi.py @@ -4,6 +4,7 @@ from __future__ import annotations +import builtins import json import sys import time @@ -1703,7 +1704,7 @@ def load_api(): with pytest.raises( RuntimeError, - match="rank-startup validation.*before MPI rank binding: cupy", + match=r"rank-startup validation.*before MPI rank binding: .*\bcupy\b", ): mpi.run_mpi_image_pair_batch( manifest_path=tmp_path / "unused.json", @@ -1910,6 +1911,35 @@ def bcast(self, value, root: int): ) +@pytest.mark.parametrize( + "import_error", + [ + ModuleNotFoundError("No module named 'mpi4py'"), + OSError("libmpi.so could not be loaded"), + RuntimeError("cannot load MPI library"), + ], +) +def test_mpi_import_failure_explains_installation( + monkeypatch: pytest.MonkeyPatch, import_error: Exception +) -> None: + original_import = builtins.__import__ + + def blocked_import(name, *args, **kwargs): + if name == "mpi4py": + raise import_error + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", blocked_import) + + with pytest.raises(RuntimeError) as error: + mpi._load_mpi_api() + + assert "pip install 'cuphoton[mpi]'" in str(error.value) + assert "MPI runtime" in str(error.value) + assert "mpiexec/mpirun" in str(error.value) + assert error.value.__cause__ is import_error + + def test_mpi_library_version_strips_trailing_nul( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/uv.lock b/uv.lock index 44b30fd8..66a0d2ed 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.11, <3.15" +requires-python = ">=3.12, <3.15" resolution-markers = [ - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'linux'", "python_full_version >= '3.14' and sys_platform == 'linux'", - "python_full_version < '3.12' and sys_platform == 'linux'", + "python_full_version < '3.14' and sys_platform == 'linux'", ] supported-markers = [ "sys_platform == 'linux'", @@ -37,6 +36,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/1e/75767a712dd23740273560df5b9dcd203362f5e5c6bea846d99a7175e233/astropy_iers_data-0.2026.6.22.1.23.34-py3-none-any.whl", hash = "sha256:d96f5102426e7ba5f28d7aabf417a547d912482afa97c24430f5c9fe9b6a884d", size = 1994066, upload-time = "2026-06-22T01:24:08.609Z" }, ] +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, +] + [[package]] name = "bokeh" version = "3.9.1" @@ -57,6 +106,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/54/40ff985187f0fd1afcedebc23eda0ba92bdbea1e0605cbb878b45391e5e0/bokeh-3.9.1-py3-none-any.whl", hash = "sha256:8bf2aae574509055fbd6b68f023046bebc91b0bbd631d204d0d60863b4237dbd", size = 6406728, upload-time = "2026-06-04T18:04:40.832Z" }, ] +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -66,6 +156,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + [[package]] name = "contourpy" version = "1.3.3" @@ -75,12 +174,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, - { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, @@ -111,8 +204,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, - { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, - { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, ] [[package]] @@ -123,8 +258,6 @@ dependencies = [ { name = "cuda-pathfinder" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" }, - { url = "https://files.pythonhosted.org/packages/95/7a/c5e3c34a409b148f5c0f5a4ea374158f95d488862c1dffedf9aa5c639df9/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04436a9364059c84b8f9636f359eccda1cf814341f5b670c71d80d2f79dbc708", size = 6674166, upload-time = "2026-05-29T23:11:45.478Z" }, { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, @@ -144,8 +277,6 @@ dependencies = [ { name = "numpy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/41/4b/4ac1d0639241da756c634add606f93a7f3a39bef12f70e1fb4b40cc53c21/cuda_core-1.0.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3effd11283bc46fd06348c2fd18a0941ba7718a6f447343858c944c1a93a6dab", size = 4784340, upload-time = "2026-05-12T20:11:23.961Z" }, - { url = "https://files.pythonhosted.org/packages/01/55/bb3e701f4af504e5e39e837135dc80022ec4c84858b2886ad577fe696a77/cuda_core-1.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1934517ff8a9dcd21b3f4a28e15e12643164b7d3ec187a4ee7560e22fd2dfc17", size = 5059041, upload-time = "2026-05-12T20:11:26.045Z" }, { url = "https://files.pythonhosted.org/packages/0d/a0/1daeae599cadd612689dbbf70d7da1c01883964fc2fbc7386f3c630a68cf/cuda_core-1.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6816dc020aee6103d8071bc02d8e4e1d91f2b49596f666896d608d92224d79d1", size = 4789856, upload-time = "2026-05-12T20:11:30.862Z" }, { url = "https://files.pythonhosted.org/packages/a1/4d/603557ab3cb171cc2a61d3678a39cb4dae3fd21275078bfbd1c0b0b5230b/cuda_core-1.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be7b65311bf78964b7905adbf3c0f8f717d432f2854dc45169277729bf60f1e2", size = 5106023, upload-time = "2026-05-12T20:11:33.509Z" }, { url = "https://files.pythonhosted.org/packages/57/f9/a6676b1fa555fad5748a945f4b530b51b898b4771a1e5d9f3520d3f415ea/cuda_core-1.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c427e5025096d96fcd5092fdc85d5d5e4ac3dea007914e90472ed52f27220446", size = 4749800, upload-time = "2026-05-12T20:11:38.012Z" }, @@ -166,22 +297,20 @@ wheels = [ [[package]] name = "cuda-tile" -version = "1.4.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/d4/a5849ee8ee58d0275c9e7738aa5b16d1ad669ed5aa4d1b26af683eda065e/cuda_tile-1.4.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:da2649de97cbaf886d564a9f75b3bd2fb112999c99c58a27c817a46bb8725f29", size = 280897, upload-time = "2026-05-27T17:44:57.197Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1b/575207f424c75e7b1608e056a89c15bfc3750f6178e5c659f693a7e822b1/cuda_tile-1.4.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:5741a789aaff85e3b2417b8611f0d11f967b9bac567432f0057b2b8bf72259ac", size = 282060, upload-time = "2026-05-27T17:44:58.877Z" }, - { url = "https://files.pythonhosted.org/packages/42/93/64ef40d3982dcda7a97ebfa3e3bb9045b573d4eb3877fa5d1fa3cd2541d3/cuda_tile-1.4.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:9e358a85a153820aa0a51d0e09346d884a3c14b88c0313d20d0fb9f53952abae", size = 280953, upload-time = "2026-05-27T17:46:53.03Z" }, - { url = "https://files.pythonhosted.org/packages/d7/9a/7fbdbdb30c375f80818941165adfc4f1dc6cebaf937c6a9081a02d5871f0/cuda_tile-1.4.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:1d9d99b6fa57366af3f8707ac4fd91411275af2ee736996a60620240fcf92070", size = 282503, upload-time = "2026-05-27T17:45:05.543Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ad/42f0655e6aee5c59015634b46d7f13bc22e74af28d10fb2008a062b37349/cuda_tile-1.4.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:fc74185efd81f6153af0a19549d111dec6861ee9b9bc27927a2cef6e19173eb5", size = 280958, upload-time = "2026-05-27T17:46:53.061Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/4770f9e36b8108ce8c9078f71eb21c65e594d79c0770dd38daa045cfbd6c/cuda_tile-1.4.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:45be74f6568c440446f510bc7799b953858e64c6abf26e96f2c9598a79084860", size = 282508, upload-time = "2026-05-27T17:45:18.515Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c6/46a329f4c56ce54471784366394e235804423df2531307e14112e4636c76/cuda_tile-1.4.0-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:738593650784ebb3c601486914b563e7569144fe596048766ea9e12280ac3bb9", size = 281208, upload-time = "2026-05-27T17:46:48.325Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fb/bf3849ad68b1858ba50e6992863d266892d7d7db02d11c485c26cd090a1b/cuda_tile-1.4.0-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:4b1a591c26836a550c2bf87c22d31c4716e5f83d24d255f843d9429625cca973", size = 282630, upload-time = "2026-05-27T17:45:10.789Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/f7f1dfa4d1ee7cc5b69e11d756be6ffec1561a5c7e3836fd0f71ca49adcf/cuda_tile-1.4.0-cp314-cp314t-manylinux2014_aarch64.whl", hash = "sha256:b3cbeffbe0fedac4936edcf00b6ba13ab5ddb74d3b7ce4a287dfc04491b5f6af", size = 283249, upload-time = "2026-05-27T17:46:12.032Z" }, - { url = "https://files.pythonhosted.org/packages/18/c0/fee527a085fca414fc993769912eb8ba2e15ce388f3168b868706e6d4c61/cuda_tile-1.4.0-cp314-cp314t-manylinux2014_x86_64.whl", hash = "sha256:675b2afff62af5d4e72c34bc72d0be27b0933a44933b8a449f590fbded8c1107", size = 284336, upload-time = "2026-05-27T17:44:59.489Z" }, + { url = "https://files.pythonhosted.org/packages/1f/cc/b4da247f624320c9898c14003e7b809b74c4a447251e1c98e33505f41757/cuda_tile-1.6.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:77fb2b15c61960514f62fc41eeae3722cf6468c051f7593fd43970691624214b", size = 378564, upload-time = "2026-09-10T00:04:22.067Z" }, + { url = "https://files.pythonhosted.org/packages/40/21/3dfeafbd3512e94a277a25ce022c10e7bdff5d43e364afffcc0928f7fcaf/cuda_tile-1.6.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:7e38f97bfedff359c926710387a40fbfc8f36efb7fe9ad2cb759d5a098eedde3", size = 382170, upload-time = "2026-09-10T00:06:48.623Z" }, + { url = "https://files.pythonhosted.org/packages/58/09/5c3b6c8fc2f03303fe97156082792f2276deb94c974f57691ce86322f91c/cuda_tile-1.6.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:43c846d903d3ed12680af28acbb936f141c71a306ac9c4b778974f81a6634550", size = 378562, upload-time = "2026-09-10T00:06:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/9c/70/879ab39f2dab93eca6c9d5caa8b5195ceccf330f95bbd8cb5f35289ee9be/cuda_tile-1.6.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:ca4858770945aa40677eff4b0bd6b0bdad73ef372d665eae4ec41202aecda830", size = 382172, upload-time = "2026-09-10T00:03:56.146Z" }, + { url = "https://files.pythonhosted.org/packages/39/3c/02f9fde3751beced12b62fd42b419fdee26e27d18255295cba1dc77f0515/cuda_tile-1.6.0-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:4785cbe370186bd3a83e8815bc9907cf186d9c5b8e24b88fd9ba4c3531374db8", size = 378978, upload-time = "2026-09-10T00:05:59.27Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/9d174b601cf5e23eaf1590b63b1eb30f21944e91b99f61e6a0908b6f28ab/cuda_tile-1.6.0-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:3dde52fd09780f3fd9f5b17d5750a94b4a98e3a18a1660e10a424ed8941fdc8d", size = 383303, upload-time = "2026-09-10T00:06:36.936Z" }, + { url = "https://files.pythonhosted.org/packages/63/c0/da38a02f6132d600cf67981960b06a823e5d9562da34ed7aea37c24d25e8/cuda_tile-1.6.0-cp314-cp314t-manylinux2014_aarch64.whl", hash = "sha256:99010df35630368e6378b950d2bdafbd6256497b8de3dd0f7ce79d5e8191fa4a", size = 380046, upload-time = "2026-09-10T00:07:01.553Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f0/2cfd157e85f8364f8797fb4e1ee9f42d7a8667706398fd2896fe2b1d2f7c/cuda_tile-1.6.0-cp314-cp314t-manylinux2014_x86_64.whl", hash = "sha256:330fdfa470740a27f2f718215635c0ea7fa46d57e1246d48df495fdf7afc582c", size = 383750, upload-time = "2026-09-10T00:04:35.678Z" }, ] [[package]] @@ -248,24 +377,27 @@ dependencies = [ { name = "numexpr" }, { name = "numpy" }, { name = "pandas" }, - { name = "photutils" }, { name = "pyarrow" }, { name = "pyyaml" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scipy" }, ] [package.optional-dependencies] cutile = [ - { name = "cuda-tile", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, - { name = "cupy-cuda13x", extra = ["ctk"], marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "cuda-tile" }, + { name = "cupy-cuda13x", extra = ["ctk"] }, ] dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "ruff" }, + { name = "setuptools" }, +] +dragon = [ + { name = "dragonhpc" }, ] gpu = [ + { name = "cuda-toolkit", extra = ["cufile"] }, { name = "cupy-cuda13x", extra = ["ctk"] }, { name = "kvikio-cu13" }, { name = "libkvikio-cu13" }, @@ -273,9 +405,23 @@ gpu = [ { name = "numba-cuda", extra = ["cu13"] }, { name = "nvidia-libnvcomp-cu13" }, { name = "nvidia-nvcomp-cu13" }, - { name = "pybind11" }, + { name = "photutils" }, { name = "torch" }, ] +io = [ + { name = "cuda-toolkit", extra = ["cufile"] }, + { name = "cupy-cuda13x", extra = ["ctk"] }, + { name = "kvikio-cu13" }, + { name = "libkvikio-cu13" }, + { name = "nvidia-libnvcomp-cu13" }, + { name = "nvidia-nvcomp-cu13" }, +] +mpi = [ + { name = "mpi4py" }, +] +photometry = [ + { name = "photutils" }, +] torch = [ { name = "torch" }, ] @@ -289,33 +435,37 @@ viz = [ requires-dist = [ { name = "astropy", specifier = ">=6.1.4" }, { name = "bokeh", marker = "extra == 'viz'", specifier = ">=3.9" }, - { name = "cuda-tile", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'linux' and extra == 'cutile'", specifier = ">=1.4" }, - { name = "cupy-cuda13x", extras = ["ctk"], marker = "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'linux' and extra == 'cutile'", specifier = ">=14,<15" }, - { name = "cupy-cuda13x", extras = ["ctk"], marker = "sys_platform == 'linux' and extra == 'gpu'", specifier = ">=14,<15" }, + { name = "cuda-tile", marker = "sys_platform == 'linux' and extra == 'cutile'", specifier = ">=1.6,<2" }, + { name = "cuda-toolkit", extras = ["cufile"], marker = "sys_platform == 'linux' and extra == 'io'", specifier = ">=13,<14" }, + { name = "cuphoton", extras = ["io", "photometry"], marker = "extra == 'gpu'" }, + { name = "cupy-cuda13x", extras = ["ctk"], marker = "sys_platform == 'linux' and extra == 'cutile'", specifier = ">=14,<15" }, + { name = "cupy-cuda13x", extras = ["ctk"], marker = "sys_platform == 'linux' and extra == 'io'", specifier = ">=14,<15" }, + { name = "dragonhpc", marker = "extra == 'dragon'", specifier = ">=0.14.2,<0.15" }, { name = "h5py", specifier = ">=3.10" }, - { name = "kvikio-cu13", marker = "sys_platform == 'linux' and extra == 'gpu'", specifier = ">=26.6,<27" }, - { name = "libkvikio-cu13", marker = "sys_platform == 'linux' and extra == 'gpu'", specifier = ">=26.6,<27" }, + { name = "kvikio-cu13", marker = "sys_platform == 'linux' and extra == 'io'", specifier = "==26.6.*" }, + { name = "libkvikio-cu13", marker = "sys_platform == 'linux' and extra == 'io'", specifier = "==26.6.*" }, + { name = "mpi4py", marker = "extra == 'mpi'", specifier = ">=4.1.2,<5" }, { name = "numba", marker = "sys_platform == 'linux' and extra == 'gpu'", specifier = ">=0.61,<0.66" }, { name = "numba-cuda", extras = ["cu13"], marker = "sys_platform == 'linux' and extra == 'gpu'", specifier = ">=0.30,<0.31" }, { name = "numexpr", specifier = ">=2.10" }, { name = "numpy", specifier = ">=2.0,<2.6" }, - { name = "nvidia-libnvcomp-cu13", marker = "sys_platform == 'linux' and extra == 'gpu'", specifier = ">=5.2,<6" }, - { name = "nvidia-nvcomp-cu13", marker = "sys_platform == 'linux' and extra == 'gpu'", specifier = ">=5.2,<6" }, + { name = "nvidia-libnvcomp-cu13", marker = "sys_platform == 'linux' and extra == 'io'", specifier = "==5.2.*" }, + { name = "nvidia-nvcomp-cu13", marker = "sys_platform == 'linux' and extra == 'io'", specifier = "==5.2.*" }, { name = "pandas", specifier = ">=2.2" }, - { name = "photutils", specifier = ">=3.0" }, + { name = "photutils", marker = "extra == 'photometry'", specifier = ">=3.0" }, { name = "pillow", marker = "extra == 'viz'", specifier = ">=10.4" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0" }, { name = "pyarrow", specifier = ">=23.0" }, - { name = "pybind11", marker = "sys_platform == 'linux' and extra == 'gpu'", specifier = ">=2.12,<4" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.12" }, { name = "scipy", specifier = ">=1.13" }, + { name = "setuptools", marker = "extra == 'dev'", specifier = ">=83.0.0" }, { name = "torch", marker = "sys_platform == 'linux' and extra == 'gpu'", specifier = ">=2.13,<3" }, { name = "torch", marker = "extra == 'torch'", specifier = ">=2.13,<3" }, { name = "tornado", marker = "extra == 'viz'", specifier = ">=6.5.10" }, ] -provides-extras = ["torch", "viz", "gpu", "cutile", "dev"] +provides-extras = ["photometry", "torch", "viz", "io", "gpu", "cutile", "mpi", "dragon", "dev"] [[package]] name = "cupy-cuda13x" @@ -326,8 +476,6 @@ dependencies = [ { name = "numpy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/35/a0/33ba64feccedf661960057a9967e2e623853f4757ba5be2f04f6503a4ac0/cupy_cuda13x-14.1.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:3d12e1020066a699f5e27f4c5597167dca042704c0ed1db74066c32c0c2dbfb1", size = 73970864, upload-time = "2026-06-01T04:53:53.903Z" }, - { url = "https://files.pythonhosted.org/packages/c1/99/d1add95f15f3ad0dec954688c012e97f991363c4a74ca970fc0ddb217534/cupy_cuda13x-14.1.1-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:424caa906c16f31328558151dd5e0596891eda7644f40ca9348c3eb14af084d1", size = 70032752, upload-time = "2026-06-01T04:54:02.944Z" }, { url = "https://files.pythonhosted.org/packages/8e/df/33e14f4648910db90d9aa3ac03c7012eec2e5d05a7ab69ec74a7864d6354/cupy_cuda13x-14.1.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:9226f3279c64aa6f07a79ba0081f3275a99a36c2db3f836f4b687dd8d77fa8bc", size = 73276230, upload-time = "2026-06-01T04:54:10.208Z" }, { url = "https://files.pythonhosted.org/packages/ad/a7/7105dad3285c35451aae7efcc0a09e5b114de25065e3971552d906354e3e/cupy_cuda13x-14.1.1-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:a57c3eda202ffd70e34b9031a88f17c810a75e816398b4028360027cb3a4cd4d", size = 69532519, upload-time = "2026-06-01T04:54:14.589Z" }, { url = "https://files.pythonhosted.org/packages/e0/0f/149a9e6c561f44e6522be6c3fdacdaed17d8fd5a1651004a0782ce045502/cupy_cuda13x-14.1.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:cc671cb766f1c42cb6eed3bf27e9da6092721bcb3d77e09aef28e2f5b089ea38", size = 72811428, upload-time = "2026-06-01T04:54:22.432Z" }, @@ -352,6 +500,25 @@ wheels = [ { 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]] +name = "dragonhpc" +version = "0.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "paramiko" }, + { name = "psutil" }, + { name = "pycapnp" }, + { name = "pyyaml" }, + { name = "shtab" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/58/0f2060acc2f1bde732ab6f23df08016f697302a0927cde7d4861767b21b3/dragonhpc-0.14.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e78f466e44fdc5faad16ba4116f238ae0669d15bc523d526c945611fb428793e", size = 15633374, upload-time = "2026-08-26T16:41:27.72Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/44a539277364edf21e95c49f49275f9bc015e2538c95cdd0a0f9d72820ee/dragonhpc-0.14.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3763d2839489dfb16d973a8a6d6cd50e31612de5ed43c8b8cfe855a2d5c5e9db", size = 15984706, upload-time = "2026-08-26T16:41:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3b/06945b01f119a30e69a0789d67336264b46221abb540b006bee7b0e461cb/dragonhpc-0.14.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c94e96fb486103ac6172ec3042d4ba48660ca64486dbbd8fc0669c2e827c3ff4", size = 15574204, upload-time = "2026-08-26T16:41:33.093Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8d/35157906acd4e9647e4dcd87ecb742d309f383e257e05c449c8f8f1c2184/dragonhpc-0.14.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9dc2c90a0902524f2e5e3c24e18fa941212e442c09aeab444e8004907528dd14", size = 15954173, upload-time = "2026-08-26T16:41:35.428Z" }, +] + [[package]] name = "filelock" version = "3.29.4" @@ -379,10 +546,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/a8/2594cef906aee761601eff842c7dc598bea2b394a3e1c00966832b8eeb7c/h5py-3.16.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a6fbc5367d4046801f9b7db9191b31895f22f1c6df1f9987d667854cac493538", size = 4823472, upload-time = "2026-03-06T13:47:53.085Z" }, - { url = "https://files.pythonhosted.org/packages/52/a0/c1f604538ff6db22a0690be2dc44ab59178e115f63c917794e529356ab23/h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3", size = 5027150, upload-time = "2026-03-06T13:47:55.043Z" }, - { url = "https://files.pythonhosted.org/packages/2e/fd/301739083c2fc4fd89950f9bcfce75d6e14b40b0ca3d40e48a8993d1722c/h5py-3.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:314b6054fe0b1051c2b0cb2df5cbdab15622fb05e80f202e3b6a5eee0d6fe365", size = 4814544, upload-time = "2026-03-06T13:47:56.893Z" }, - { url = "https://files.pythonhosted.org/packages/4c/42/2193ed41ccee78baba8fcc0cff2c925b8b9ee3793305b23e1f22c20bf4c7/h5py-3.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ffbab2fedd6581f6aa31cf1639ca2cb86e02779de525667892ebf4cc9fd26434", size = 5034013, upload-time = "2026-03-06T13:47:59.01Z" }, { url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852, upload-time = "2026-03-06T13:48:07.482Z" }, { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250, upload-time = "2026-03-06T13:48:09.628Z" }, { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108, upload-time = "2026-03-06T13:48:11.26Z" }, @@ -419,6 +582,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "invoke" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/227c48c5fe47fa178ccf1fda8f047d16c97ba926567b661e9ce2045c600c/invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c", size = 343419, upload-time = "2026-04-07T15:17:48.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/de/bbc12563bbf979618d17625a4e753ff7a078523e28d870d3626daa97261a/invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053", size = 160958, upload-time = "2026-04-07T15:17:46.875Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -464,8 +636,6 @@ version = "0.47.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/27/5799b020e4cdfb25a7c951c06a96397c135efcdc21b78d853bbd9c814c7d/llvmlite-0.47.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca14f02e29134e837982497959a8e2193d6035235de1cb41a9cb2bd6da4eedbb", size = 56275177, upload-time = "2026-03-31T18:28:31.01Z" }, - { url = "https://files.pythonhosted.org/packages/7e/51/48a53fedf01cb1f3f43ef200be17ebf83c8d9a04018d3783c1a226c342c2/llvmlite-0.47.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12a69d4bb05f402f30477e21eeabe81911e7c251cecb192bed82cd83c9db10d8", size = 55128631, upload-time = "2026-03-31T18:28:36.046Z" }, { url = "https://files.pythonhosted.org/packages/e6/4b/e3f2cd17822cf772a4a51a0a8080b0032e6d37b2dbe8cfb724eac4e31c52/llvmlite-0.47.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd", size = 56275178, upload-time = "2026-03-31T18:28:48.342Z" }, { url = "https://files.pythonhosted.org/packages/b6/55/a3b4a543185305a9bdf3d9759d53646ed96e55e7dfd43f53e7a421b8fbae/llvmlite-0.47.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077", size = 55128632, upload-time = "2026-03-31T18:28:52.901Z" }, { url = "https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2699a74321189e812d476a43d6d7f652f51811e7b5aad9d9bba842a1c7927acb", size = 56275178, upload-time = "2026-03-31T18:29:05.748Z" }, @@ -482,12 +652,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, @@ -520,6 +684,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, ] +[[package]] +name = "mpi4py" +version = "4.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/83/231445bbcf7ef10864746c244ff2d82000011449b79275642c5d4ed8c8f4/mpi4py-4.1.2.tar.gz", hash = "sha256:56860286dc45f20e8821e93cb06669e30462348bf866f685553fa4b712d58d02", size = 501709, upload-time = "2026-05-16T10:35:23.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/0a/1da7f403e0d8ce0e26d541f7538302cec00cf5b0a98a7a52b929f938a25c/mpi4py-4.1.2-cp310-abi3-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2ef63b2e3083e6062fd90e4de8c4e3acbf81e0772406e0226eb8dde6a48cab8e", size = 1327130, upload-time = "2026-05-16T10:34:00.269Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f9/65999152ae82bad914c6a083821ee774afefd6d0544e633b940c9a9ebf3f/mpi4py-4.1.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6508e654b9c8ff9f611b19548b2a17d1e323b520a15168189f92221e6757b8ff", size = 1182268, upload-time = "2026-05-16T10:34:02.206Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2b/1e48c4c5f9acbdca8dd28beeba9123dde140cd2ca520f8e3a3cf22faeeaa/mpi4py-4.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:00f4cce8999d19f35243c3442ea22debbe3336f69c309cd5d3176df4e51c717a", size = 1358844, upload-time = "2026-05-16T10:34:28.247Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/a37225d47997fcf30adca25d3849d035bbb61d972118b024db900306e528/mpi4py-4.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0a58e164776acb7b52414548b1bf0e5caafce0ee90345deff147873a64b6b2cc", size = 1227206, upload-time = "2026-05-16T10:34:29.866Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5e/493415cdb0da0c1c0b6ec9a1fb65ab57a174e8111b25c87f7507f663d0b4/mpi4py-4.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:085e0cb05427398fe2281856f27a87984b9f234cd6d98a2a384a6fbfe679a56f", size = 1358571, upload-time = "2026-05-16T10:34:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/6f/51/3822e834fc9cafd96811501b85232e02bd9b31596d42965536a269a6c112/mpi4py-4.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:429437883b4511583d56a57bc58ad0d02965fb6c19f84f39e30c0646e6c0cab9", size = 1226562, upload-time = "2026-05-16T10:34:39.652Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5e/d358fadb8672d58abd6dce16c95eda56f497b378dec5642a31cd6ededfc4/mpi4py-4.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85e332feaac3323d8ed1c71f17478fe3f529468f28f6f9bb4c9133ad2d3a3a6d", size = 1419007, upload-time = "2026-05-16T10:34:45.47Z" }, + { url = "https://files.pythonhosted.org/packages/27/eb/5cd53880337009cab9a9d17a007ad5aec731f3a55e211bdcc99dbb98a0e3/mpi4py-4.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e05dba75f6ad17ae761bdc01ee5c5271d15cd296e8a508e22c99c91b540dc99", size = 1298482, upload-time = "2026-05-16T10:34:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/26/e4/32575e9b0d5380b08ffa04958c7a13b1bab86e3674f33b6de3827fbbe14c/mpi4py-4.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d53e8f7182bf125b37155f679b301a21cd64e5ae6861ca0ab989c9d2b2073bbd", size = 1368388, upload-time = "2026-05-16T10:34:53.665Z" }, + { url = "https://files.pythonhosted.org/packages/87/a8/ad5e925da9de402704ecfe8715348b068fb0b65059109c65dafb111f95e4/mpi4py-4.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cbf982d3425fd06890fbcff79ba19a7435e93bc74e5711ca8ac0b64d0c3ece9", size = 1238211, upload-time = "2026-05-16T10:34:55.094Z" }, + { url = "https://files.pythonhosted.org/packages/39/29/45a41d081342896af52d38a848017728bf6834c4c92814c50aa791dc750b/mpi4py-4.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db189238c37be98933eeb078a3e2b827eef969837e8fe0f3a2e6cdb7f4b1b05f", size = 1423092, upload-time = "2026-05-16T10:35:01.672Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/c2127d426d87f2f7cdb4beacbb4febe7b0c93047a3c615d0e56cecb42563/mpi4py-4.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d0fbf0bc100ed966c3d7f44231a19aef0025d523d1b813de468a1b114d0d9436", size = 1303134, upload-time = "2026-05-16T10:35:03.122Z" }, +] + [[package]] name = "mpmath" version = "1.3.0" @@ -566,8 +750,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz", hash = "sha256:19357146c32fe9ed25059ab915e8465fb13951cf6b0aace3826b76886373ab23", size = 2765600, upload-time = "2026-04-24T02:02:56.551Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/0b/0615dbedb98f5b32a35a53290fbdc6e22306968109278d7e58df82d7a9f6/numba-0.65.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f80ed83774b5173abd6581cd8d2165d1d38e13d2e5c8155c0c0b421784745420", size = 3745018, upload-time = "2026-04-24T02:02:20.252Z" }, - { url = "https://files.pythonhosted.org/packages/49/aa/4361698f35bf63bff67dfe6c90493731177f48ede954f77b0588731537bc/numba-0.65.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ed425a43b0a5f9772f2f4e2dd0bbd12eabecae1af0b24efcfd4e053f012aac6", size = 3450962, upload-time = "2026-04-24T02:02:22.449Z" }, { url = "https://files.pythonhosted.org/packages/69/47/a415af0283e4db0398104c6d1c11c9861a98dc67a7aa442a7769ed5d6196/numba-0.65.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:52bc6f3ceb8fcaff9b2ae26b4c6b1e9fee39db8d355534c0fe4f39a901246b84", size = 3802467, upload-time = "2026-04-24T02:02:27.712Z" }, { url = "https://files.pythonhosted.org/packages/46/36/246f73ec99cfeab2f2cb2ce7d4218766cc36a2da418901223f4f4da9c813/numba-0.65.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ca10b3463bae0bd70589726fe3c77d01d6b5fc86bee54bcdf9fb6b47c28977", size = 3502628, upload-time = "2026-04-24T02:02:29.763Z" }, { url = "https://files.pythonhosted.org/packages/a0/22/b8d873f6466b20aa563fc9b33acd48dec89a07803ddaa2f1c8ca1cd33126/numba-0.65.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c09f49117ef255e1f1c6dad0c7a1ed39868243862a73be5706793241a3755f1b", size = 3810619, upload-time = "2026-04-24T02:02:36.041Z" }, @@ -590,8 +772,6 @@ dependencies = [ { name = "packaging" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e5/600d083ac18e61f7fc65bf0055f7897b0ce2d695d4041748292baed8b195/numba_cuda-0.30.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3a5cb81ada38b3a0c4f4a3bdc0c594205764ae0eeb2d2d3d7993b1faaf100d9", size = 1861063, upload-time = "2026-06-25T19:07:23.573Z" }, - { url = "https://files.pythonhosted.org/packages/08/e7/c0cf52487b57ac72c8cbaf9a9e544175dd1b21d28cd3abdb8a60f36889d9/numba_cuda-0.30.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbfb2885fd4829d2db2c7603be514230ad6b2f9c16b68947636e2801e1e1e9a6", size = 1857789, upload-time = "2026-06-25T19:07:25.05Z" }, { url = "https://files.pythonhosted.org/packages/f9/a0/d79b1a0b74ab3117eaf82452138d5701cb24fe88f4bb3d7fb0dcc45541bd/numba_cuda-0.30.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4878a8276a703800d832bcc2e23634a1143dab7c6d56f5fb021fa4f24040d54e", size = 1901967, upload-time = "2026-06-25T19:07:28.405Z" }, { url = "https://files.pythonhosted.org/packages/83/49/9c72a93c0854b36d244b185f7cbf51f7e991850ac1fbf3d6e3850d89f864/numba_cuda-0.30.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cce9b218b9b607fe5bf0169b813163c98aab15326ffc2dceb77ac42ad0ac862e", size = 1899532, upload-time = "2026-06-25T19:07:29.997Z" }, { url = "https://files.pythonhosted.org/packages/06/2d/a472bb65a7c23c8b0ea2ea9fd6148ea26148ca0c91409eb0df1d2a92977f/numba_cuda-0.30.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10cd3d3bbc53ba627ac28a00ca8c33bd0a8e90133fbbe0b43924e40007a6d7fe", size = 1909121, upload-time = "2026-06-25T19:07:33.278Z" }, @@ -616,10 +796,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/cb/2f/fdba158c9dbe5caca9c3eca3eaffffb251f2fb8674bf8e2d0aed5f38d319/numexpr-2.14.1.tar.gz", hash = "sha256:4be00b1086c7b7a5c32e31558122b7b80243fe098579b170967da83f3152b48b", size = 119400, upload-time = "2025-10-13T16:17:27.351Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/7f/3bae417cb13ae08afd86d08bb0301c32440fe0cae4e6262b530e0819aeda/numexpr-2.14.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebe4980f9494b9f94d10d2e526edc29e72516698d3bf95670ba79415492212a4", size = 451126, upload-time = "2025-10-13T16:13:22.248Z" }, - { url = "https://files.pythonhosted.org/packages/4c/1a/edbe839109518364ac0bd9e918cf874c755bb2c128040e920f198c494263/numexpr-2.14.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a381e5e919a745c9503bcefffc1c7f98c972c04ec58fc8e999ed1a929e01ba6", size = 442012, upload-time = "2025-10-13T16:14:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/b1/be4ce99bff769a5003baddac103f34681997b31d4640d5a75c0e8ed59c78/numexpr-2.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d08856cfc1b440eb1caaa60515235369654321995dd68eb9377577392020f6cb", size = 1415975, upload-time = "2025-10-13T16:13:26.088Z" }, - { url = "https://files.pythonhosted.org/packages/e7/33/b33b8fdc032a05d9ebb44a51bfcd4b92c178a2572cd3e6c1b03d8a4b45b2/numexpr-2.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03130afa04edf83a7b590d207444f05a00363c9b9ea5d81c0f53b1ea13fad55a", size = 1464683, upload-time = "2025-10-13T16:14:58.87Z" }, { url = "https://files.pythonhosted.org/packages/72/94/cc921e35593b820521e464cbbeaf8212bbdb07f16dc79fe283168df38195/numexpr-2.14.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d686dfb2c1382d9e6e0ee0b7647f943c1886dba3adbf606c625479f35f1956c1", size = 452468, upload-time = "2025-10-13T16:13:29.531Z" }, { url = "https://files.pythonhosted.org/packages/d9/43/560e9ba23c02c904b5934496486d061bcb14cd3ebba2e3cf0e2dccb6c22b/numexpr-2.14.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee6d4fbbbc368e6cdd0772734d6249128d957b3b8ad47a100789009f4de7083", size = 443631, upload-time = "2025-10-13T16:15:02.473Z" }, { url = "https://files.pythonhosted.org/packages/7b/6c/78f83b6219f61c2c22d71ab6e6c2d4e5d7381334c6c29b77204e59edb039/numexpr-2.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a2839efa25f3c8d4133252ea7342d8f81226c7c4dda81f97a57e090b9d87a48", size = 1417670, upload-time = "2025-10-13T16:13:33.464Z" }, @@ -648,10 +824,6 @@ version = "2.4.6" source = { registry = "https://pypi.org/simple" } 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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, - { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, - { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, @@ -672,8 +844,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, - { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, ] [[package]] @@ -886,10 +1056,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2", size = 10770558, upload-time = "2026-05-11T18:52:19.865Z" }, - { url = "https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27", size = 11274611, upload-time = "2026-05-11T18:52:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824", size = 11784670, upload-time = "2026-05-11T18:52:25.4Z" }, - { url = "https://files.pythonhosted.org/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938", size = 12353708, upload-time = "2026-05-11T18:52:28.139Z" }, { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, @@ -912,6 +1078,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, ] +[[package]] +name = "paramiko" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "cryptography" }, + { name = "invoke" }, + { name = "pynacl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/93/dcc25d52f49022ae6175d15e6bd751f1acc99b98bc61fc55e5155a7be2e7/paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79", size = 1548586, upload-time = "2026-05-09T18:28:52.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/5b/eadf6d45de38d30ab603f49393b6cd2cbe7e233af8cf90197e32782b68a9/paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c", size = 208919, upload-time = "2026-05-09T18:28:50.295Z" }, +] + [[package]] name = "photutils" version = "3.0.0" @@ -919,8 +1100,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astropy" }, { name = "numpy" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scipy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2c/54/9eb5c73edb7fe3b911af0c64e8022f5b629444e34816a975bd0efa60b758/photutils-3.0.0.tar.gz", hash = "sha256:5e1ca2be9433e1e1f9f6d477e1e6c8029755caceefb4e349048955e3cb825529", size = 969607, upload-time = "2026-04-17T22:14:25.206Z" } wheels = [ @@ -933,10 +1113,6 @@ version = "12.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/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/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" }, @@ -959,8 +1135,6 @@ wheels = [ { 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/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" }, ] [[package]] @@ -997,16 +1171,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, +] + [[package]] name = "pyarrow" version = "24.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/8e/fb178720400ef69db251eb4a9c3ccf4af269bc1feb5055529b8fc87170d1/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", size = 45697931, upload-time = "2026-04-21T10:46:48.403Z" }, - { url = "https://files.pythonhosted.org/packages/f3/27/99c42abe8e21b44f4917f62631f3aa31404882a2c41d8a4cd5c110e13d52/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", size = 48837449, upload-time = "2026-04-21T10:46:55.329Z" }, - { url = "https://files.pythonhosted.org/packages/36/b6/333749e2666e9032891125bf9c691146e92901bece62030ac1430e2e7c88/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", size = 49395949, upload-time = "2026-04-21T10:47:01.869Z" }, - { url = "https://files.pythonhosted.org/packages/17/25/c5201706a2dd374e8ba6ee3fd7a8c89fb7ffc16eed5217a91fd2bd7f7626/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", size = 51912986, upload-time = "2026-04-21T10:47:09.872Z" }, { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, @@ -1030,12 +1216,36 @@ wheels = [ ] [[package]] -name = "pybind11" -version = "3.0.4" +name = "pycapnp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/86/a57e3c92acd3e1d2fc3dcad683ada191f722e4ac927e1a384b228ec2780a/pycapnp-2.1.0.tar.gz", hash = "sha256:69cc3d861fee1c9b26c73ad2e8a5d51e76ad87e4ff9be33a4fd2fc72f5846aec", size = 689734, upload-time = "2025-09-05T03:50:40.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/0a/9ee1c9ecaff499e4fd1df2f0335bc20f666ec6ce5cd80f8ab055007f3c9b/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:568e79268ba7c02a71fe558a8aec1ae3c0f0e6aff809ff618a46afe4964957d2", size = 5143502, upload-time = "2025-09-05T03:48:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/4d/50/65837e1416f7a8861ca1e8fe4582a5aef37192d7ef5e2ecfe46880bfdf9c/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:bcbf6f882d78d368c8e4bb792295392f5c4d71ddffa13a48da27e7bd47b99e37", size = 5508134, upload-time = "2025-09-05T03:48:59.383Z" }, + { url = "https://files.pythonhosted.org/packages/a1/59/46df6db800e77dbc3cc940723fb3fd7bc837327c858edf464a0f904bf547/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:dc25b96e393410dde25c61c1df3ce644700ef94826c829426d58c2c6b3e2d2f5", size = 5631794, upload-time = "2025-09-05T03:49:03.511Z" }, + { url = "https://files.pythonhosted.org/packages/63/9d/18e978500d5f6bd8d152f4d6919e3cfb83ead8a71c14613bbb54322df8b9/pycapnp-2.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:48938e0436ab1be615fc0a41434119a2065490a6212b9a5e56949e89b0588b76", size = 5369378, upload-time = "2025-09-05T03:49:05.539Z" }, + { url = "https://files.pythonhosted.org/packages/96/dc/726f1917e9996dc29f9fd1cf30674a14546cdbdfa0777e1982b6bd1ad628/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c20de0f6e0b3fa9fa1df3864cf46051db3511b63bc29514d1092af65f2b82a0", size = 5999140, upload-time = "2025-09-05T03:49:07.341Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3a/3bbc4c5776fc32fbf8a59df5c7c5810efd292b933cd6545eb4b16d896268/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:18caca6527862475167c10ea0809531130585aa8a86cc76cd1629eb87ee30637", size = 6454308, upload-time = "2025-09-05T03:49:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/bf/dd/17e2d7808424f10ffddc47329b980488ed83ec716c504791787e593a7a93/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9dcc11237697007b66e3bfc500d2ad892bd79672c9b50d61fbf728c6aaf936de", size = 6544212, upload-time = "2025-09-05T03:49:10.675Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/68090013128d7853f34c43828dd4dc80a7c8516fd1b56057b134e1e4c2c0/pycapnp-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c151edf78155b6416e7cb31e2e333d302d742ba52bb37d4dbdf71e75cc999d46", size = 6295279, upload-time = "2025-09-05T03:49:12.712Z" }, + { url = "https://files.pythonhosted.org/packages/65/ad/75536d0117fd282f7c896685a641c88c1a0333670113ae146806982e4ebb/pycapnp-2.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a7c9a43af6416f0c4b77b54111a05ac70758adfb19a9128b4d00709889b0a4bc", size = 5146057, upload-time = "2025-09-05T03:49:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/0f1c2d7e595431da2ded71fbba79ef1ece7398454109bcfd1a2e31524211/pycapnp-2.1.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1b2173afc1bc75e42fc8039ea53693c966f106f6f33aa42c17dd03f239312cc1", size = 5506174, upload-time = "2025-09-05T03:49:21.752Z" }, + { url = "https://files.pythonhosted.org/packages/1c/58/c431f503606b80bc90374254305a5c6941f9364bc73165a2da9f0517a78d/pycapnp-2.1.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:b130e7ead8e1ec5d3110895dc550728258a3f26ab563632c4a8a9d8f0c271f0d", size = 5565869, upload-time = "2025-09-05T03:49:23.461Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a24b33ae757a30df73b1e57123d4a4ea70b03f220d7812e2faa68c7e0c52/pycapnp-2.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ae50ae6a4240b673f88947f0e945f9dd459e4335249049c224ef16c812abb5d6", size = 5325427, upload-time = "2025-09-05T03:49:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/2a/85/7341282d30990b11e4159ccbfb06e7c58d0b575032e5412ed5d9b7e31b6c/pycapnp-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab155aba8bec6add4ebf314d52bfccf9c9f9b94570abcd2c03fc837c02d0b67d", size = 6012977, upload-time = "2025-09-05T03:49:27.336Z" }, + { url = "https://files.pythonhosted.org/packages/c1/29/3645c346aa7a8338b2ec998bab38f4ed2268ab2da0ccbbf91e71aa624535/pycapnp-2.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2fedd70ff9463cd7877ee74dda249dcf69fcd4ab26eacbdfae5aae0342def329", size = 6450946, upload-time = "2025-09-05T03:49:34.662Z" }, + { url = "https://files.pythonhosted.org/packages/f7/19/4e36d9f31cd58138bf39a45e7280469bf64c896cd638a4764acde14d7b5a/pycapnp-2.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6715920564b071c6931ed82f949f707a20ccff74e50f3bbdcd48b7734d6ac8aa", size = 6504420, upload-time = "2025-09-05T03:49:39.643Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ad/dee371ce3422a503ea84e0c5afffb38922e9ab845284fcf4872c2c3c17ff/pycapnp-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:246accc884fa5c7de40dec84f6b767ddf1718c3148bf223317ee0a23ba37b188", size = 6265048, upload-time = "2025-09-05T03:49:43.333Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/f0/35145a3c3baffeef55d4b8324caa33abaa8fa56ab345ecd4b2211d09163e/pybind11-3.0.4.tar.gz", hash = "sha256:3286b59c8a774b9ee650169302dd5a4eedc30a8617905a0560dd8ee44775130c", size = 589533, upload-time = "2026-04-19T03:08:15.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/06/c3a23c9a0263b136c519f033a58d4641e73065fefc7754e9667ec206d992/pybind11-3.0.4-py3-none-any.whl", hash = "sha256:961720ee652da51d531b7b2451a6bd2bc042b0106e6d9baa48ecb7d58034ce63", size = 314166, upload-time = "2026-04-19T03:08:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] @@ -1061,6 +1271,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -1107,11 +1344,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, @@ -1163,52 +1395,10 @@ wheels = [ { 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" }, ] -[[package]] -name = "scipy" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.12' and sys_platform == 'linux'", -] -dependencies = [ - { name = "numpy" }, -] -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 = [ - { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, - { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, - { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, - { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, - { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, - { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, - { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, - { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, - { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, - { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, - { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, - { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, - { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, - { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, - { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, - { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, -] - [[package]] name = "scipy" version = "1.18.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'linux'", -] dependencies = [ { name = "numpy" }, ] @@ -1241,6 +1431,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, ] +[[package]] +name = "shtab" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/71/ddb3c0a7a86db44d2fb3f9cbac162f7ddbcbf563b4a174963ba2b3d4d819/shtab-1.12.1.tar.gz", hash = "sha256:0637338723a8fc08ed1c2fd826d8432229924649c26e3247bb48c53d60ca3bf9", size = 66195, upload-time = "2026-09-01T22:12:47.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/3f/8bf03f51e920585e207157cb49c5bf8e65a9229297c9ec347a6014eb3d61/shtab-1.12.1-py3-none-any.whl", hash = "sha256:31d07db9c958fbe15edd1d0f3c966a7085766374a11ccc5861b810fcd4ada123", size = 22763, upload-time = "2026-09-01T22:12:46.384Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1283,8 +1482,6 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/59/1e3160e18e12aa3038390efab3ce02b36a9d4d6a527ecdd8520dca2e68d8/torch-2.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c", size = 427199369, upload-time = "2026-07-08T16:04:51.054Z" }, - { url = "https://files.pythonhosted.org/packages/01/79/1f2d34ad7034ee1c7ffc1cf8bf0f8213af2a81df6ecdb3997ecec107c09d/torch-2.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7", size = 526574961, upload-time = "2026-07-08T16:04:07.075Z" }, { url = "https://files.pythonhosted.org/packages/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" }, { url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" }, { url = "https://files.pythonhosted.org/packages/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" }, @@ -1312,8 +1509,6 @@ name = "triton" version = "3.7.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/f9/19d842d06a08559534fa1eaab6ca551b1bcf40f06620bddec1babaa2772d/triton-3.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a0e1cd4c4a76370ed74a8432a53cea28716827d19e40ffc732233e35ceb3f6", size = 184664887, upload-time = "2026-06-17T20:03:42.913Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5e/fce69606f7f240297f163e25539906732b199530d486ce67ae319877e821/triton-3.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6744957e9fd610a29680ec2346057d0c86948ed3812468670719f391e94b44a5", size = 197701306, upload-time = "2026-06-17T19:53:13.673Z" }, { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" }, { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" }, { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" },