From 61c421b75f6a8aeb0c3fb07595269dd6c3f03f31 Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Wed, 23 Sep 2026 16:08:18 -0700 Subject: [PATCH 01/11] Initialize the shared XDR I/O pool only once Concurrent readers could reset the same KvikIO thread pool while it was active. Serialize initialization and honor KVIKIO_NTHREADS so subsequent reads reuse the established pool. Signed-off-by: Trent Nelson --- src/cuphoton/xdr/gds.py | 25 +++++--- tests/xdr/test_gds.py | 94 ++++++++++++++++++++++++++++ tests/xdr/test_reader_concurrency.py | 2 + 3 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 tests/xdr/test_gds.py 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/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..4ac7a28b 100644 --- a/tests/xdr/test_reader_concurrency.py +++ b/tests/xdr/test_reader_concurrency.py @@ -572,6 +572,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 = [] From 3c615949a93d26f34aacb051432de05dffd7a896 Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Wed, 23 Sep 2026 16:08:43 -0700 Subject: [PATCH 02/11] Build Linux wheels with native XDR and private CFITSIO Package the extension for CPython 3.12 through 3.14 on x86-64 and ARM64. Pin the native build inputs and keep CUDA runtime libraries in their upstream wheels. Add installed-wheel and artifact checks, and preserve the CFITSIO notice and license expression in repaired wheels. Separate I/O dependencies from the full GPU profile and make Photutils optional so ARM64 I/O installations do not require a compiler. Signed-off-by: Trent Nelson --- CONTRIBUTING.md | 4 +- MANIFEST.in | 1 + Makefile | 17 +- README.md | 44 ++- THIRD_PARTY_NOTICES.md | 72 ++-- docs/components/xdr.md | 49 ++- docs/getting-started.md | 34 +- pyproject.toml | 39 +- scripts/wheels/build-requirements.txt | 19 + scripts/wheels/check_distributions.py | 226 +++++++++++ scripts/wheels/install_build_dependencies.sh | 11 + scripts/wheels/prepare_cfitsio.sh | 29 ++ scripts/wheels/repair_wheel.py | 144 +++++++ scripts/wheels/test_installed.py | 396 +++++++++++++++++++ src/cuphoton/_photometry.py | 8 +- src/cuphoton/xdr/nvcomp_batch.py | 45 +-- src/cuphoton/xdr/setup_package.py | 43 +- src/cuphoton/xdr/src/build.sh | 4 +- tests/test_package_layout.py | 5 +- tests/xdr/test_build_script.py | 2 +- tests/xdr/test_cuda_discovery.py | 166 ++++++++ tests/xdr/test_reader_concurrency.py | 5 +- uv.lock | 165 ++------ 23 files changed, 1258 insertions(+), 270 deletions(-) create mode 100644 scripts/wheels/build-requirements.txt create mode 100755 scripts/wheels/check_distributions.py create mode 100755 scripts/wheels/install_build_dependencies.sh create mode 100755 scripts/wheels/prepare_cfitsio.sh create mode 100755 scripts/wheels/repair_wheel.py create mode 100644 scripts/wheels/test_installed.py create mode 100644 tests/xdr/test_cuda_discovery.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a695394f..4c63dc01 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 ``` @@ -73,7 +73,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..0986bf89 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,6 +11,7 @@ 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..b4b2ecd9 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 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: @@ -70,7 +70,10 @@ 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 +82,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..926b7a1f 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,23 +133,39 @@ 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 for the base, GPU, CPU PyTorch, +and visualization profiles. The experimental cuTile profile remains limited +to Python 3.12 and 3.13. | 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 | +| `gpu` | The `io` and `photometry` extras plus CUDA 13 PyTorch and Numba-CUDA | | `cutile` | Experimental `cuda.tile` backend 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. For an installed release: + +```bash +python -m pip install cuphoton # CPU data workflows +python -m pip install 'cuphoton[io]' # GPU FITS loading +python -m pip install 'cuphoton[gpu]' # All GPU backends and photometry +``` + +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]' @@ -163,10 +180,9 @@ uv sync --locked --python 3.12 --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 +226,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..7bff28e2 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; 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. + +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` | `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` | @@ -111,16 +118,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 +199,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..bc1fbcc8 100644 --- a/docs/components/xdr.md +++ b/docs/components/xdr.md @@ -25,28 +25,38 @@ reader by default. ## Install -Install the CUDA 13 development profile: +Install the I/O profile for GPU FITS loading: ```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 +94,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/getting-started.md b/docs/getting-started.md index a8c88415..4f6f8cda 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -2,16 +2,32 @@ ## 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. Python +3.12 or 3.13 is required for the experimental cuTile profile. 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 + +```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 +44,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,8 +58,10 @@ 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 | | `viz` | Bokeh and Pillow | @@ -72,7 +90,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/pyproject.toml b/pyproject.toml index 97d583b8..7004b478 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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,15 +49,18 @@ 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 = [ @@ -64,6 +68,7 @@ cutile = [ "cupy-cuda13x[ctk]>=14,<15; python_version >= '3.12' and python_version < '3.14' and platform_system == 'Linux'", ] dev = [ + "setuptools>=83.0.0", "pre-commit>=4.0", "pytest>=8.3", "ruff>=0.15.12", @@ -102,7 +107,7 @@ environments = ["sys_platform == 'linux'"] [tool.ruff] line-length = 78 -target-version = "py311" +target-version = "py312" force-exclude = true extend-exclude = [ "*.ipynb", @@ -128,3 +133,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..fa53473b --- /dev/null +++ b/scripts/wheels/build-requirements.txt @@ -0,0 +1,19 @@ +# 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 +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/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/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/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..feb97349 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' 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..87c76274 100644 --- a/tests/test_package_layout.py +++ b/tests/test_package_layout.py @@ -57,7 +57,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 () @@ -66,6 +66,8 @@ def test_distribution_metadata_declares_supported_profiles() -> None: "cutile", "dev", "gpu", + "io", + "photometry", "torch", "viz", } @@ -134,6 +136,7 @@ def test_cli_help_and_version_do_not_import_optional_gpu_packages( "kvikio", "mpi4py", "numba", + "photutils", "torch", ) 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_reader_concurrency.py b/tests/xdr/test_reader_concurrency.py index 4ac7a28b..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 diff --git a/uv.lock b/uv.lock index 44b30fd8..816cf2e9 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'", @@ -75,12 +74,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 +104,6 @@ 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]] @@ -123,8 +114,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 +133,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" }, @@ -172,8 +159,6 @@ 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" }, @@ -248,24 +233,24 @@ 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", marker = "python_full_version < '3.14'" }, + { name = "cupy-cuda13x", extra = ["ctk"], marker = "python_full_version < '3.14'" }, ] dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "ruff" }, + { name = "setuptools" }, ] gpu = [ + { name = "cuda-toolkit", extra = ["cufile"] }, { name = "cupy-cuda13x", extra = ["ctk"] }, { name = "kvikio-cu13" }, { name = "libkvikio-cu13" }, @@ -273,9 +258,20 @@ 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" }, +] +photometry = [ + { name = "photutils" }, +] torch = [ { name = "torch" }, ] @@ -290,32 +286,34 @@ 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 = "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 = "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 = "cupy-cuda13x", extras = ["ctk"], marker = "sys_platform == 'linux' and extra == 'io'", specifier = ">=14,<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 = "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", "dev"] [[package]] name = "cupy-cuda13x" @@ -326,8 +324,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" }, @@ -379,10 +375,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" }, @@ -464,8 +456,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 +472,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" }, @@ -566,8 +550,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 +572,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 +596,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 +624,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 +644,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 +856,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" }, @@ -919,8 +885,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 +898,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 +920,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]] @@ -1003,10 +962,6 @@ 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" }, @@ -1029,15 +984,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, ] -[[package]] -name = "pybind11" -version = "3.0.4" -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" } -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" }, -] - [[package]] name = "pyerfa" version = "2.0.1.5" @@ -1107,11 +1053,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 +1104,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" }, ] @@ -1283,8 +1182,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 +1209,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" }, From dc5235761eb9fe2e9737dff018049dd756ad39aa Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Wed, 23 Sep 2026 16:08:44 -0700 Subject: [PATCH 03/11] Validate and publish the native wheel matrix in CI Signed-off-by: Trent Nelson --- .github/workflows/ci.yml | 163 +++------------------------------- .github/workflows/publish.yml | 108 ++++++++++++++++++++++ .github/workflows/wheels.yml | 130 +++++++++++++++++++++++++++ docs/packaging.md | 98 ++++++++++++++++++++ 4 files changed, 347 insertions(+), 152 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/wheels.yml create mode 100644 docs/packaging.md 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..3dd067ce --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,108 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +name: publish + +on: + workflow_dispatch: + inputs: + run-id: + description: Successful push CI run containing the qualified distributions + required: true + type: string + version: + description: Release version matching an existing v-prefixed tag + required: true + type: string + target: + description: Protected publishing environment + required: true + type: choice + options: [testpypi, pypi] + default: testpypi + +permissions: + contents: read + +concurrency: + group: publish-${{ inputs.target }} + cancel-in-progress: false + +jobs: + prepare: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + actions: read + contents: read + 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" + - name: Verify CI provenance and release tag + env: + GH_TOKEN: ${{ github.token }} + RUN_ID: ${{ inputs.run-id }} + RELEASE_VERSION: ${{ inputs.version }} + run: | + gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}" > run.json + python - <<'PY' + import json + import os + import re + import subprocess + + version = os.environ['RELEASE_VERSION'] + assert re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+(?:rc[0-9]+)?', version), version + run = json.load(open('run.json')) + assert run['conclusion'] == 'success', run['conclusion'] + assert run['event'] == 'push', run['event'] + assert run['path'] == '.github/workflows/ci.yml', run['path'] + assert run['head_branch'] in ('main', '0.1.x'), run['head_branch'] + assert run['head_repository']['full_name'] == os.environ['GITHUB_REPOSITORY'] + tag = subprocess.check_output( + ['git', 'rev-parse', f'refs/tags/v{version}^{{commit}}'], text=True + ).strip() + assert tag == run['head_sha'], (tag, run['head_sha']) + PY + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cuphoton-distributions + path: dist + run-id: ${{ inputs.run-id }} + github-token: ${{ github.token }} + - name: Verify all six wheels and the source archive + env: + RELEASE_VERSION: ${{ inputs.version }} + run: | + python scripts/wheels/check_distributions.py dist --version "$RELEASE_VERSION" + uvx --isolated --from twine==6.2.0 twine check --strict dist/* + sha256sum dist/* >> "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: publish-distributions + path: dist/* + if-no-files-found: error + + publish: + needs: prepare + runs-on: ubuntu-24.04 + timeout-minutes: 10 + environment: + name: ${{ inputs.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: ${{ inputs.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..cda89322 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,130 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +name: native wheels + +on: + workflow_call: + 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 + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + with: + version: "0.12.1" + - run: 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" + - 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: + needs: wheels + strategy: + fail-fast: false + matrix: + python: ["3.12", "3.13", "3.14"] + 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 + - 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 }} + run: | + docker run --rm \ + -e WHEEL_ABI="cp${PYTHON_VERSION/./}" \ + -v "$PWD/wheelhouse:/wheels:ro" \ + -v "$PWD/scripts/wheels:/checks:ro" \ + "python:${PYTHON_VERSION}-slim-bookworm" \ + 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 + ' + + distributions: + needs: [sdist, wheels, install] + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + with: + version: "0.12.1" + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: cuphoton-* + merge-multiple: true + 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 diff --git a/docs/packaging.md b/docs/packaging.md new file mode 100644 index 00000000..137d80c7 --- /dev/null +++ b/docs/packaging.md @@ -0,0 +1,98 @@ +# 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. + +## 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. + +## 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. + +## Publish the qualified artifacts + +Configure PyPI and TestPyPI Trusted Publishers for this repository, +`publish.yml`, and the respective `pypi` / `testpypi` GitHub environments. +Require a reviewer for each environment and restrict publishing to `main`. +No stored API token is needed. See the [PyPA Trusted Publishing workflow +guide](https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/). + +1. Select a successful **push** run of `ci.yml` on `main` or `0.1.x` and + download its `cuphoton-distributions` artifact before its 30-day expiry. +2. Qualify those exact binaries on both GPU architectures. Retain their + hashes and results for the environment reviewer. +3. Create the approved `v` tag at that run's source commit. +4. Dispatch `publish.yml` from `main`, supplying that run ID, version, and + `testpypi`. The workflow verifies the CI provenance, tag, complete matrix, + and metadata, and displays the artifact hashes before environment approval. +5. Verify TestPyPI downloads against those hashes and repeat the dispatch + with `pypi` after release approval. + +Publication downloads and uploads the verified CI artifacts without rebuilding +them. A successful CI run does not replace GPU qualification or release +approval. The publishing workflow itself does not create tags or releases. From cffcf42196433f1e018dbb016d2d3bfcbdca09dd Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Wed, 23 Sep 2026 20:34:13 -0700 Subject: [PATCH 04/11] Derive package versions from release tags Signed-off-by: Trent Nelson --- .gitignore | 1 + MANIFEST.in | 2 + pyproject.toml | 16 +- scripts/wheels/build-requirements.txt | 2 + src/cuphoton/__init__.py | 6 +- src/cuphoton/xdr/src/build.sh | 2 +- tests/test_versioning.py | 221 ++++++++++++++++++++++++++ 7 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 tests/test_versioning.py 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/MANIFEST.in b/MANIFEST.in index 0986bf89..b00237b7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,8 @@ 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 diff --git a/pyproject.toml b/pyproject.toml index 7004b478..c2b370ba 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] @@ -88,8 +88,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"] diff --git a/scripts/wheels/build-requirements.txt b/scripts/wheels/build-requirements.txt index fa53473b..e93e431d 100644 --- a/scripts/wheels/build-requirements.txt +++ b/scripts/wheels/build-requirements.txt @@ -4,6 +4,8 @@ # 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 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/xdr/src/build.sh b/src/cuphoton/xdr/src/build.sh index feb97349..efbb703e 100755 --- a/src/cuphoton/xdr/src/build.sh +++ b/src/cuphoton/xdr/src/build.sh @@ -39,7 +39,7 @@ fi cd "$ROOT_DIR" printf 'Building xdr with Python: %s\n' "$PYTHON" uv pip install --python "$PYTHON" \ - 'setuptools>=83.0.0' wheel 'pybind11>=3.0,<4' + '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 \ 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") From 712e76db7e0c4aac47dbd5153bb1b940da3788a7 Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Wed, 23 Sep 2026 20:34:14 -0700 Subject: [PATCH 05/11] Build and publish tag releases and release candidates Signed-off-by: Trent Nelson --- .github/workflows/publish.yml | 123 +++++++---- .github/workflows/wheels.yml | 47 ++++- docs/packaging.md | 102 +++++++-- scripts/wheels/release.py | 237 +++++++++++++++++++++ tests/test_release.py | 378 ++++++++++++++++++++++++++++++++++ 5 files changed, 824 insertions(+), 63 deletions(-) create mode 100755 scripts/wheels/release.py create mode 100644 tests/test_release.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3dd067ce..7e169bc2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,14 +5,12 @@ name: publish on: + push: + tags: ["v*"] workflow_dispatch: inputs: - run-id: - description: Successful push CI run containing the qualified distributions - required: true - type: string version: - description: Release version matching an existing v-prefixed tag + description: Existing release tag version, such as 0.1.3rc0 required: true type: string target: @@ -21,22 +19,69 @@ on: 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 }} + group: publish-${{ inputs.target || 'pypi' }}-${{ github.event_name == 'push' && github.ref_name || format('v{0}', inputs.version) }} cancel-in-progress: false jobs: prepare: - if: github.ref == 'refs/heads/main' + 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 || 'pypi' }} + 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: @@ -45,57 +90,55 @@ jobs: - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 with: version: "0.12.1" - - name: Verify CI provenance and release tag - env: - GH_TOKEN: ${{ github.token }} - RUN_ID: ${{ inputs.run-id }} - RELEASE_VERSION: ${{ inputs.version }} - run: | - gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}" > run.json - python - <<'PY' - import json - import os - import re - import subprocess - - version = os.environ['RELEASE_VERSION'] - assert re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+(?:rc[0-9]+)?', version), version - run = json.load(open('run.json')) - assert run['conclusion'] == 'success', run['conclusion'] - assert run['event'] == 'push', run['event'] - assert run['path'] == '.github/workflows/ci.yml', run['path'] - assert run['head_branch'] in ('main', '0.1.x'), run['head_branch'] - assert run['head_repository']['full_name'] == os.environ['GITHUB_REPOSITORY'] - tag = subprocess.check_output( - ['git', 'rev-parse', f'refs/tags/v{version}^{{commit}}'], text=True - ).strip() - assert tag == run['head_sha'], (tag, run['head_sha']) - PY - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: cuphoton-distributions path: dist - run-id: ${{ inputs.run-id }} + run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ github.token }} - - name: Verify all six wheels and the source archive + - 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: - RELEASE_VERSION: ${{ inputs.version }} + 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/* - sha256sum dist/* >> "$GITHUB_STEP_SUMMARY" + 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 + 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: ${{ inputs.target }} + name: ${{ needs.prepare.outputs.target }} permissions: id-token: write steps: @@ -105,4 +148,4 @@ jobs: path: dist - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 with: - repository-url: ${{ inputs.target == 'testpypi' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }} + 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 index cda89322..684ca002 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -6,6 +6,17 @@ 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: @@ -19,10 +30,19 @@ jobs: - 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" - - run: CUPHOTON_XDR_BUILD_EXT=0 uv build --sdist + - 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 @@ -81,6 +101,7 @@ jobs: - 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 }} @@ -112,13 +133,21 @@ jobs: - 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" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - pattern: cuphoton-* - merge-multiple: true + 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/* @@ -128,3 +157,15 @@ jobs: 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/docs/packaging.md b/docs/packaging.md index 137d80c7..2dbc2dea 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -18,6 +18,39 @@ 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: @@ -76,23 +109,52 @@ testing the minimum CUDA runtime, constrain `cuda-toolkit==13.0.3.0` and ## Publish the qualified artifacts -Configure PyPI and TestPyPI Trusted Publishers for this repository, -`publish.yml`, and the respective `pypi` / `testpypi` GitHub environments. -Require a reviewer for each environment and restrict publishing to `main`. -No stored API token is needed. See the [PyPA Trusted Publishing workflow -guide](https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/). - -1. Select a successful **push** run of `ci.yml` on `main` or `0.1.x` and - download its `cuphoton-distributions` artifact before its 30-day expiry. -2. Qualify those exact binaries on both GPU architectures. Retain their - hashes and results for the environment reviewer. -3. Create the approved `v` tag at that run's source commit. -4. Dispatch `publish.yml` from `main`, supplying that run ID, version, and - `testpypi`. The workflow verifies the CI provenance, tag, complete matrix, - and metadata, and displays the artifact hashes before environment approval. -5. Verify TestPyPI downloads against those hashes and repeat the dispatch - with `pypi` after release approval. - -Publication downloads and uploads the verified CI artifacts without rebuilding -them. A successful CI run does not replace GPU qualification or release -approval. The publishing workflow itself does not create tags or releases. +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 a reviewer for each GitHub environment and allow deployments from +branch `main` and tags matching `v*`. Keep self-review available when the release +operator is the sole reviewer. 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 `pypi` environment for approval. 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. To rehearse on TestPyPI before approving PyPI, dispatch `publish.yml` from + `main` with the same `version`, `target=testpypi`, and the original release + `run-id`. This reuses its artifacts even while its PyPI job awaits approval. +4. Verify the TestPyPI downloads against the recorded hashes, then approve the + original PyPI job. Alternatively, dispatch with `target=pypi` and the same + original build run ID to promote the identical files. + +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/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/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) From d5c1db102bdfdb21219a8aa36413ec11c03fa470 Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Wed, 23 Sep 2026 20:44:57 -0700 Subject: [PATCH 06/11] Clarify installation before the first PyPI release Signed-off-by: Trent Nelson --- README.md | 3 ++- docs/components/xdr.md | 3 ++- docs/getting-started.md | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 926b7a1f..218d10e3 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,8 @@ to Python 3.12 and 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. For an installed release: +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 diff --git a/docs/components/xdr.md b/docs/components/xdr.md index bc1fbcc8..a49f04fd 100644 --- a/docs/components/xdr.md +++ b/docs/components/xdr.md @@ -25,7 +25,8 @@ reader by default. ## Install -Install the I/O profile for GPU FITS loading: +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 python -m pip install 'cuphoton[io]' diff --git a/docs/getting-started.md b/docs/getting-started.md index 4f6f8cda..f385a8e0 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -13,6 +13,9 @@ 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 From 263eeabd6c0adccf1bc81de6aeb977987d294b07 Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Wed, 23 Sep 2026 20:47:37 -0700 Subject: [PATCH 07/11] Clarify replacing a waiting publication run Signed-off-by: Trent Nelson --- docs/packaging.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/packaging.md b/docs/packaging.md index 2dbc2dea..60f56cbe 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -142,8 +142,10 @@ workflow. `main` with the same `version`, `target=testpypi`, and the original release `run-id`. This reuses its artifacts even while its PyPI job awaits approval. 4. Verify the TestPyPI downloads against the recorded hashes, then approve the - original PyPI job. Alternatively, dispatch with `target=pypi` and the same - original build run ID to promote the identical files. + original PyPI job. Alternatively, cancel that waiting job before dispatching + with `target=pypi` and the same original build run ID. Publishing runs for + one version and destination are serialized, so leaving the original waiting + would block the replacement. The replacement promotes the identical files. Manual dispatch without `run-id` builds the supplied existing tag and publishes to the selected environment after approval. Dispatch with `run-id` always uses From fd2a9854fb82e581549530278680485d15dd2b42 Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Wed, 23 Sep 2026 20:54:48 -0700 Subject: [PATCH 08/11] Keep release tooling caches local to each job Signed-off-by: Trent Nelson --- .github/workflows/publish.yml | 1 + .github/workflows/wheels.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7e169bc2..51afe301 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -90,6 +90,7 @@ jobs: - 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 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 684ca002..a5290f58 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -35,6 +35,7 @@ jobs: - 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 }} @@ -72,6 +73,7 @@ jobs: - 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 @@ -137,6 +139,7 @@ jobs: - 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 From 46ea6f1a52a8ac7a16a8bd104dec322bd15be0da Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Thu, 24 Sep 2026 13:36:11 -0700 Subject: [PATCH 09/11] Add MPI and Dragon installation extras Make distributed runtimes selectable through pip and uv, and explain the remaining MPI and Dragon platform requirements at use time. Enable cuTile across the supported Python versions and document its compiler setup alongside the GPU runtime dependencies. Signed-off-by: Trent Nelson --- CONTRIBUTING.md | 5 +- README.md | 20 +- THIRD_PARTY_NOTICES.md | 15 +- docs/components/xpois.md | 41 ++-- docs/getting-started.md | 55 ++++- pyproject.toml | 10 +- src/cuphoton/core/_mpi_runtime.py | 7 +- src/cuphoton/core/dragon.py | 5 +- tests/test_package_layout.py | 41 +++- tests/xpois/test_dragon.py | 32 +++ tests/xpois/test_mpi.py | 32 ++- uv.lock | 328 ++++++++++++++++++++++++++++-- 12 files changed, 521 insertions(+), 70 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c63dc01..ecec66f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index 218d10e3..0a362dbc 100644 --- a/README.md +++ b/README.md @@ -133,9 +133,9 @@ 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: -CPython 3.12 through 3.14 is supported on Linux for the base, GPU, CPU PyTorch, -and visualization profiles. The experimental cuTile profile remains limited -to 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 | | --- | --- | @@ -144,7 +144,9 @@ to Python 3.12 and 3.13. | `io` | CUDA 13 CuPy, KvikIO, cuFile, and nvCOMP for XDR | | `torch` | PyTorch workflows that can be forced to CPU execution | | `gpu` | The `io` and `photometry` extras plus CUDA 13 PyTorch and Numba-CUDA | -| `cutile` | Experimental `cuda.tile` backend on Python 3.12 or 3.13 | +| `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, @@ -154,7 +156,9 @@ 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]' # All GPU backends and photometry +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, @@ -172,11 +176,11 @@ python -m pip install -e '.[dev,torch,viz,photometry]' 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. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 7bff28e2..3f767e77 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -5,8 +5,8 @@ optional-development, and build requirements are declared in [`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; it does not include the separately installed DragonHPC, -`mpi4py`, or MPI runtimes. Build-system requirements are resolved separately +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. @@ -56,7 +56,7 @@ more than one license. | `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` | @@ -66,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. 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 f385a8e0..c3656cb5 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -4,8 +4,9 @@ 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. Python -3.12 or 3.13 is required for the experimental cuTile profile. +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 @@ -66,15 +67,61 @@ The extras are composable: | `torch` | CPU-capable PyTorch | | `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 diff --git a/pyproject.toml b/pyproject.toml index c2b370ba..f713936f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,8 +64,14 @@ gpu = [ "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", 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/tests/test_package_layout.py b/tests/test_package_layout.py index 87c76274..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 @@ -65,23 +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: 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 816cf2e9..66a0d2ed 100644 --- a/uv.lock +++ b/uv.lock @@ -36,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" @@ -56,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" @@ -65,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" @@ -106,6 +206,50 @@ wheels = [ { 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" }, ] +[[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]] name = "cuda-bindings" version = "13.3.1" @@ -153,20 +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/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]] @@ -240,8 +384,8 @@ dependencies = [ [package.optional-dependencies] cutile = [ - { name = "cuda-tile", marker = "python_full_version < '3.14'" }, - { name = "cupy-cuda13x", extra = ["ctk"], marker = "python_full_version < '3.14'" }, + { name = "cuda-tile" }, + { name = "cupy-cuda13x", extra = ["ctk"] }, ] dev = [ { name = "pre-commit" }, @@ -249,6 +393,9 @@ dev = [ { name = "ruff" }, { name = "setuptools" }, ] +dragon = [ + { name = "dragonhpc" }, +] gpu = [ { name = "cuda-toolkit", extra = ["cufile"] }, { name = "cupy-cuda13x", extra = ["ctk"] }, @@ -269,6 +416,9 @@ io = [ { name = "nvidia-libnvcomp-cu13" }, { name = "nvidia-nvcomp-cu13" }, ] +mpi = [ + { name = "mpi4py" }, +] photometry = [ { name = "photutils" }, ] @@ -285,14 +435,16 @@ 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 = "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 = "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 == '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 == '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" }, @@ -313,7 +465,7 @@ requires-dist = [ { name = "torch", marker = "extra == 'torch'", specifier = ">=2.13,<3" }, { name = "tornado", marker = "extra == 'viz'", specifier = ">=6.5.10" }, ] -provides-extras = ["photometry", "torch", "viz", "io", "gpu", "cutile", "dev"] +provides-extras = ["photometry", "torch", "viz", "io", "gpu", "cutile", "mpi", "dragon", "dev"] [[package]] name = "cupy-cuda13x" @@ -348,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" @@ -411,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" @@ -504,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" @@ -878,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" @@ -956,6 +1171,22 @@ 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" @@ -984,6 +1215,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, ] +[[package]] +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/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/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pyerfa" version = "2.0.1.5" @@ -1007,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" @@ -1140,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" From a0cfa9cd6750ec6ceb1b94204c4ea86e89906a5e Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Thu, 24 Sep 2026 13:36:34 -0700 Subject: [PATCH 10/11] Exercise installed wheels with real compute runtimes Run MPI and Dragon workers in the native wheel CI matrix and check cuTile imports across every supported Python version. Keep Dragon's unavailable Python 3.14 wheel explicit. Add GPU acceptance checks for cuTile, Numba, PyTorch, and the actual MPI and Dragon batch executors, alongside the native FITS checks. Signed-off-by: Trent Nelson --- .github/workflows/wheels.yml | 81 ++++++- Makefile | 7 +- docs/packaging.md | 45 ++++ scripts/wheels/test_stack.py | 420 +++++++++++++++++++++++++++++++++++ tests/test_wheel_stack.py | 61 +++++ 5 files changed, 611 insertions(+), 3 deletions(-) create mode 100644 scripts/wheels/test_stack.py create mode 100644 tests/test_wheel_stack.py diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index a5290f58..99a7fdd2 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -127,8 +127,87 @@ jobs: 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} + - {version: "3.13", dragon: true} + # Dragon 0.14.2 has no Python 3.14 wheel or source distribution. + - {version: "3.14", dragon: false} + 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 }} + 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:${PYTHON_VERSION}-slim-bookworm" \ + sh -ec ' + apt-get update + apt-get install -y --no-install-recommends openmpi-bin libopenmpi3 + 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] + needs: [sdist, wheels, install, runtimes] runs-on: ubuntu-24.04 timeout-minutes: 10 steps: diff --git a/Makefile b/Makefile index b4b2ecd9..3fbde645 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # # 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 wheels 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 --extra photometry GPU_EXTRAS = --extra dev --extra gpu --extra viz @@ -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,6 +66,9 @@ 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 diff --git a/docs/packaging.md b/docs/packaging.md index 60f56cbe..c59ad97a 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -81,6 +81,13 @@ 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 @@ -107,6 +114,44 @@ 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: 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/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) From 90c2259eaf447c802ba6f78270dbd03d44d0bb03 Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Thu, 24 Sep 2026 17:40:14 -0700 Subject: [PATCH 11/11] Require explicit PyPI promotion and document install changes Signed-off-by: Trent Nelson --- .github/workflows/publish.yml | 4 ++-- .github/workflows/wheels.yml | 32 ++++++++++++++++++++++++-------- CHANGELOG.md | 10 ++++++++++ docs/packaging.md | 29 ++++++++++++++++------------- 4 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 51afe301..e3b8f82b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,7 +28,7 @@ permissions: contents: read concurrency: - group: publish-${{ inputs.target || 'pypi' }}-${{ github.event_name == 'push' && github.ref_name || format('v{0}', inputs.version) }} + group: publish-${{ inputs.target || 'testpypi' }}-${{ github.event_name == 'push' && github.ref_name || format('v{0}', inputs.version) }} cancel-in-progress: false jobs: @@ -42,7 +42,7 @@ jobs: version: ${{ steps.release.outputs.version }} sha: ${{ steps.release.outputs.sha }} tag: ${{ steps.release.outputs.tag }} - target: ${{ inputs.target || 'pypi' }} + target: ${{ inputs.target || 'testpypi' }} steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 99a7fdd2..c3c50504 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -89,11 +89,18 @@ jobs: if-no-files-found: error install: + name: installed wheel / py${{ matrix.python.version }} / ${{ matrix.platform.arch }} needs: wheels strategy: fail-fast: false matrix: - python: ["3.12", "3.13", "3.14"] + 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} @@ -110,13 +117,14 @@ jobs: path: wheelhouse - name: Install without a compiler, toolkit, or system CFITSIO env: - PYTHON_VERSION: ${{ matrix.python }} + 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:${PYTHON_VERSION}-slim-bookworm" \ + "$PYTHON_IMAGE" \ sh -ec ' set -- /wheels/*-"${WHEEL_ABI}"-*.whl test "$#" = 1 @@ -134,10 +142,16 @@ jobs: fail-fast: false matrix: python: - - {version: "3.12", dragon: true} - - {version: "3.13", dragon: true} + - 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} + - 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} @@ -155,6 +169,7 @@ jobs: - 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 @@ -169,10 +184,11 @@ jobs: -v "$PWD/wheelhouse:/wheels:ro" \ -v "$PWD/scripts/wheels:/checks:ro" \ -v "$PWD/stack-reports:/reports" \ - "python:${PYTHON_VERSION}-slim-bookworm" \ + "$PYTHON_IMAGE" \ sh -ec ' apt-get update - apt-get install -y --no-install-recommends openmpi-bin libopenmpi3 + 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 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/docs/packaging.md b/docs/packaging.md index c59ad97a..950b3ede 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -164,17 +164,20 @@ Configure these Trusted Publishers in the respective package-index accounts: | Workflow filename | `publish.yml` | `publish.yml` | | GitHub environment | `pypi` | `testpypi` | -Require a reviewer for each GitHub environment and allow deployments from -branch `main` and tags matching `v*`. Keep self-review available when the release -operator is the sole reviewer. Project owners register each publisher on its -index; GitHub environment configuration alone does not grant upload access. +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 `pypi` environment for approval. No release tags are created by the +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 @@ -183,14 +186,14 @@ workflow. 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. To rehearse on TestPyPI before approving PyPI, dispatch `publish.yml` from - `main` with the same `version`, `target=testpypi`, and the original release - `run-id`. This reuses its artifacts even while its PyPI job awaits approval. -4. Verify the TestPyPI downloads against the recorded hashes, then approve the - original PyPI job. Alternatively, cancel that waiting job before dispatching - with `target=pypi` and the same original build run ID. Publishing runs for - one version and destination are serialized, so leaving the original waiting - would block the replacement. The replacement promotes the identical files. +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