From 4977accd665653eaf8e788b920d14c6324147b8b Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 28 Jul 2026 11:17:54 +0100 Subject: [PATCH 01/10] Consolidate Python writers into publishable spatialdata-js-util MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge `spatialdata-codec-writer` (image recompression) and `spatialdata-experimental-writer` (Morton points indexing) into a single distribution suitable for PyPI, and add the two capabilities they were missing: reading our stores back in Python, and CSC table matrices. Zarr reader shim Register `experimental.openjph_htj2k`, `experimental.imagecodecs_htj2k`, and `imagecodecs_jpeg2k` as zarr-python v3 codecs via `zarr.codecs` entry points, so `spatialdata.read_zarr` opens stores this package writes with no import or setup beyond installing the distribution. HTJ2K backend selection `imagecodecs` (native OpenJPH 0.30.1) now serves as a Node-free backend alongside the vendored openjph-wasm build. Measured against the WASM encoder it is byte-exact for lossless at 1/2/3/8 components and within ±1 LSB for lossy, and `level` is the same qstep as our `quality`. Multi-component correctness is not assumed from the library name — the WASM build previously used here silently decoded every component as component 0 (docs/multi-component-codec-findings.md). A backend is admitted only if it decodes a committed multi-component codestream, produced by the WASM encoder, to the exact expected samples. Manifests now record `encoder: "openjph"` with the build in `encoder_backend`, since either backend may have produced the bytes. CSC tables `tables to-csc` converts `X` and named layers from CSR to CSC, so reading one gene is a contiguous range read rather than a scan of every row. Two things this had to get right: AnnData's writer drops SpatialData's element attributes (`region`, `version`, …), which makes `read_zarr` fail, so they are captured and merged back; and anndata >=0.12 exposes `X` as a `None`-keyed layer alias that must not be converted twice. Also: single `spatialdata-js-util` CLI grouped as images/points/tables/ codecs/tui, spatialdata pinned to >=0.8.0, and write-time dependencies moved behind a `[write]` extra so the reader shim installs light. Co-Authored-By: Claude Opus 5 --- .cursor/settings.json | 2 +- .github/workflows/test.yml | 6 +- .gitignore | 4 +- .vscode/settings.json | 2 +- package.json | 6 +- .../core/tests/mortonPointsTiling.spec.ts | 4 +- .../core/tests/parquetFooterStats.spec.ts | 2 +- packages/core/tests/pointsFeatures.spec.ts | 2 +- .../tests/vtableDirectoryResponse.spec.ts | 2 +- packages/core/tests/vtableMultipart.spec.ts | 2 +- python/spatialdata-codec-writer/README.md | 218 --- .../spatialdata-codec-writer/pyproject.toml | 46 - .../src/spatialdata_codec_writer/__init__.py | 29 - .../src/spatialdata_codec_writer/cli.py | 137 -- .../src/spatialdata_codec_writer/codecs.py | 172 -- .../spatialdata_codec_writer/synthetic_cli.py | 17 - .../spatialdata-experimental-writer/README.md | 132 -- .../pyproject.toml | 37 - .../__init__.py | 17 - .../spatialdata_experimental_writer/cli.py | 348 ---- .../spatialdata-experimental-writer/uv.lock | 394 ---- .../docs/htj2k-wasm-encode-design.md | 0 .../docs/multi-component-codec-findings.md | 0 python/spatialdata-js-util/pyproject.toml | 83 + .../scripts/benchmark_points_index.py | 0 .../scripts/build_htj2k_probe.py | 71 + .../scripts/fixture_writer.py | 13 +- .../scripts/generate_codec_fixtures.py | 4 +- .../scripts/htj2k_fixtures.py | 11 +- .../scripts/mandelbulb_fixtures.py | 2 +- .../scripts/provenance.py | 0 .../scripts/synthetic_images.py | 0 .../scripts/write_synthetic.py | 6 +- .../src/spatialdata_js_util/__init__.py | 73 + .../src/spatialdata_js_util/cli.py | 565 ++++++ .../spatialdata_js_util/codecs/__init__.py | 61 + .../spatialdata_js_util/codecs/backends.py | 215 +++ .../src/spatialdata_js_util/codecs/chunks.py | 40 + .../spatialdata_js_util/codecs/encoding.py | 111 ++ .../spatialdata_js_util/codecs/htj2k_wasm.py} | 28 +- .../src/spatialdata_js_util/codecs/names.py | 26 + .../codecs/probe/multicomponent.j2c | Bin 0 -> 423 bytes .../codecs/probe/multicomponent.npy | Bin 0 -> 1280 bytes .../codecs}/vendor/__init__.py | 0 .../codecs}/vendor/encode-plane.mjs | 0 .../spatialdata_js_util/codecs/zarr_codec.py | 138 ++ .../src/spatialdata_js_util}/errors.py | 0 .../src/spatialdata_js_util/images.py} | 72 +- .../index_permutations.py | 2 +- .../src/spatialdata_js_util}/points.py | 0 .../src/spatialdata_js_util/provenance.py | 17 + .../src/spatialdata_js_util}/runners.py | 35 +- .../src/spatialdata_js_util/store.py} | 87 +- .../src/spatialdata_js_util/tables.py | 260 +++ .../src/spatialdata_js_util}/tui/__init__.py | 0 .../src/spatialdata_js_util}/tui/app.py | 0 .../src/spatialdata_js_util}/tui/models.py | 0 .../src/spatialdata_js_util}/tui/screens.py | 2 +- .../src/spatialdata_js_util}/verify.py | 0 .../tests/conftest.py | 0 .../tests/test_htj2k_encode.py | 22 +- .../tests/test_htj2k_encode_demo.py | 4 +- .../tests/test_htj2k_quality.py | 6 +- .../tests/test_integration.py | 22 +- .../tests/test_points.py | 2 +- .../tests/test_recompress.py | 23 +- .../tests/test_synthetic_images.py | 0 .../spatialdata-js-util/tests/test_tables.py | 161 ++ .../tests/test_verify.py | 8 +- .../tests/test_write_synthetic.py | 4 +- .../tests/test_writer.py | 12 +- .../tests/test_zarr.py | 2 +- .../tests/test_zarr_codec.py | 169 ++ .../uv.lock | 1615 +++++++++-------- scripts/vendor-openjph-for-python.mjs | 7 +- tests/integration/codecFixtures.test.ts | 2 +- 76 files changed, 3148 insertions(+), 2412 deletions(-) delete mode 100644 python/spatialdata-codec-writer/README.md delete mode 100644 python/spatialdata-codec-writer/pyproject.toml delete mode 100644 python/spatialdata-codec-writer/src/spatialdata_codec_writer/__init__.py delete mode 100644 python/spatialdata-codec-writer/src/spatialdata_codec_writer/cli.py delete mode 100644 python/spatialdata-codec-writer/src/spatialdata_codec_writer/codecs.py delete mode 100644 python/spatialdata-codec-writer/src/spatialdata_codec_writer/synthetic_cli.py delete mode 100644 python/spatialdata-experimental-writer/README.md delete mode 100644 python/spatialdata-experimental-writer/pyproject.toml delete mode 100644 python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/__init__.py delete mode 100644 python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/cli.py delete mode 100644 python/spatialdata-experimental-writer/uv.lock rename python/{spatialdata-codec-writer => spatialdata-js-util}/docs/htj2k-wasm-encode-design.md (100%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/docs/multi-component-codec-findings.md (100%) create mode 100644 python/spatialdata-js-util/pyproject.toml rename python/{spatialdata-experimental-writer => spatialdata-js-util}/scripts/benchmark_points_index.py (100%) create mode 100644 python/spatialdata-js-util/scripts/build_htj2k_probe.py rename python/{spatialdata-codec-writer => spatialdata-js-util}/scripts/fixture_writer.py (98%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/scripts/generate_codec_fixtures.py (96%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/scripts/htj2k_fixtures.py (96%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/scripts/mandelbulb_fixtures.py (95%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/scripts/provenance.py (100%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/scripts/synthetic_images.py (100%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/scripts/write_synthetic.py (96%) create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/__init__.py create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/cli.py create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/codecs/__init__.py create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/codecs/backends.py create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/codecs/chunks.py create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/codecs/encoding.py rename python/{spatialdata-codec-writer/src/spatialdata_codec_writer/htj2k_encode.py => spatialdata-js-util/src/spatialdata_js_util/codecs/htj2k_wasm.py} (93%) create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/codecs/names.py create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/codecs/probe/multicomponent.j2c create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/codecs/probe/multicomponent.npy rename python/{spatialdata-codec-writer/src/spatialdata_codec_writer => spatialdata-js-util/src/spatialdata_js_util/codecs}/vendor/__init__.py (100%) rename python/{spatialdata-codec-writer/src/spatialdata_codec_writer => spatialdata-js-util/src/spatialdata_js_util/codecs}/vendor/encode-plane.mjs (100%) create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/codecs/zarr_codec.py rename python/{spatialdata-experimental-writer/src/spatialdata_experimental_writer => spatialdata-js-util/src/spatialdata_js_util}/errors.py (100%) rename python/{spatialdata-codec-writer/src/spatialdata_codec_writer/recompress.py => spatialdata-js-util/src/spatialdata_js_util/images.py} (93%) rename python/{spatialdata-experimental-writer/src/spatialdata_experimental_writer => spatialdata-js-util/src/spatialdata_js_util}/index_permutations.py (99%) rename python/{spatialdata-experimental-writer/src/spatialdata_experimental_writer => spatialdata-js-util/src/spatialdata_js_util}/points.py (100%) create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/provenance.py rename python/{spatialdata-experimental-writer/src/spatialdata_experimental_writer => spatialdata-js-util/src/spatialdata_js_util}/runners.py (92%) rename python/{spatialdata-experimental-writer/src/spatialdata_experimental_writer/zarr.py => spatialdata-js-util/src/spatialdata_js_util/store.py} (63%) create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/tables.py rename python/{spatialdata-experimental-writer/src/spatialdata_experimental_writer => spatialdata-js-util/src/spatialdata_js_util}/tui/__init__.py (100%) rename python/{spatialdata-experimental-writer/src/spatialdata_experimental_writer => spatialdata-js-util/src/spatialdata_js_util}/tui/app.py (100%) rename python/{spatialdata-experimental-writer/src/spatialdata_experimental_writer => spatialdata-js-util/src/spatialdata_js_util}/tui/models.py (100%) rename python/{spatialdata-experimental-writer/src/spatialdata_experimental_writer => spatialdata-js-util/src/spatialdata_js_util}/tui/screens.py (99%) rename python/{spatialdata-experimental-writer/src/spatialdata_experimental_writer => spatialdata-js-util/src/spatialdata_js_util}/verify.py (100%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/tests/conftest.py (100%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/tests/test_htj2k_encode.py (75%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/tests/test_htj2k_encode_demo.py (93%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/tests/test_htj2k_quality.py (88%) rename python/{spatialdata-experimental-writer => spatialdata-js-util}/tests/test_integration.py (91%) rename python/{spatialdata-experimental-writer => spatialdata-js-util}/tests/test_points.py (98%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/tests/test_recompress.py (96%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/tests/test_synthetic_images.py (100%) create mode 100644 python/spatialdata-js-util/tests/test_tables.py rename python/{spatialdata-experimental-writer => spatialdata-js-util}/tests/test_verify.py (95%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/tests/test_write_synthetic.py (89%) rename python/{spatialdata-codec-writer => spatialdata-js-util}/tests/test_writer.py (93%) rename python/{spatialdata-experimental-writer => spatialdata-js-util}/tests/test_zarr.py (98%) create mode 100644 python/spatialdata-js-util/tests/test_zarr_codec.py rename python/{spatialdata-codec-writer => spatialdata-js-util}/uv.lock (56%) diff --git a/.cursor/settings.json b/.cursor/settings.json index 55a61b03..e1f9b920 100644 --- a/.cursor/settings.json +++ b/.cursor/settings.json @@ -10,7 +10,7 @@ }, "python.analysis.extraPaths": [ "${workspaceFolder}/python", - "${workspaceFolder}/python/spatialdata-codec-writer/scripts" + "${workspaceFolder}/python/spatialdata-js-util/scripts" ] } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 03da78dd..4395b882 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -105,7 +105,7 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Vendor OpenJPH for spatialdata-codec-writer + - name: Vendor OpenJPH for spatialdata-js-util run: node scripts/vendor-openjph-for-python.mjs - name: Build packages @@ -128,8 +128,8 @@ jobs: if: steps.cache-fixtures.outputs.cache-hit != 'true' run: pnpm test:fixtures:generate - - name: Run spatialdata-codec-writer tests - run: uv run --directory python/spatialdata-codec-writer pytest + - name: Run spatialdata-js-util tests + run: uv run --directory python/spatialdata-js-util --extra write pytest - name: Run unit tests run: pnpm test:unit diff --git a/.gitignore b/.gitignore index 9810296d..8d061b57 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,8 @@ python/v0.5.0/.venv/ python/v0.6.1/.venv/ python/v0.7.2/.venv/ -# Vendored from @cornerstonejs/codec-openjph (see scripts/vendor-openjph-for-python.mjs) -python/spatialdata-codec-writer/src/spatialdata_codec_writer/vendor/openjph/ +# Vendored from openjph-wasm (see scripts/vendor-openjph-for-python.mjs) +python/spatialdata-js-util/src/spatialdata_js_util/codecs/vendor/openjph/ **/__pycache__/ *.egg-info/ diff --git a/.vscode/settings.json b/.vscode/settings.json index f28034d0..8d383584 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,7 +10,7 @@ }, "python.analysis.extraPaths": [ "${workspaceFolder}/python", - "${workspaceFolder}/python/spatialdata-codec-writer/scripts" + "${workspaceFolder}/python/spatialdata-js-util/scripts" ], "typescript.tsdk": "node_modules/typescript/lib", "js/ts.experimental.useTsgo": false diff --git a/package.json b/package.json index 7e563e35..c6151c75 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,9 @@ "test:fixtures:generate:0.5.0": "uv run python/scripts/generate_fixtures.py --version 0.5.0", "test:fixtures:generate:0.6.1": "uv run python/scripts/generate_fixtures.py --version 0.6.1", "test:fixtures:generate:0.7.2": "uv run python/scripts/generate_fixtures.py --version 0.7.2", - "test:fixtures:generate:codecs": "node scripts/vendor-openjph-for-python.mjs && UV_CACHE_DIR=.tmp/uv-cache uv run --directory python/spatialdata-codec-writer python scripts/generate_codec_fixtures.py --output-dir ../../test-fixtures/codecs --experimental-htj2k --overwrite", - "write-synthetic": "node scripts/vendor-openjph-for-python.mjs && uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer-write-synthetic", - "test:codec-writer": "node scripts/vendor-openjph-for-python.mjs && UV_CACHE_DIR=.tmp/uv-cache uv run --directory python/spatialdata-codec-writer pytest", + "test:fixtures:generate:codecs": "node scripts/vendor-openjph-for-python.mjs && UV_CACHE_DIR=.tmp/uv-cache uv run --directory python/spatialdata-js-util --extra write python scripts/generate_codec_fixtures.py --output-dir ../../test-fixtures/codecs --experimental-htj2k --overwrite", + "write-synthetic": "node scripts/vendor-openjph-for-python.mjs && uv run --directory python/spatialdata-js-util --extra write python scripts/write_synthetic.py", + "test:python": "node scripts/vendor-openjph-for-python.mjs && UV_CACHE_DIR=.tmp/uv-cache uv run --directory python/spatialdata-js-util --extra write pytest", "test:server": "node scripts/test-server.js", "test:proxy": "node scripts/cors-proxy.js", "validate:datasets:js": "node scripts/validate-datasets-js.js", diff --git a/packages/core/tests/mortonPointsTiling.spec.ts b/packages/core/tests/mortonPointsTiling.spec.ts index 00f81bab..684c8546 100644 --- a/packages/core/tests/mortonPointsTiling.spec.ts +++ b/packages/core/tests/mortonPointsTiling.spec.ts @@ -8,7 +8,7 @@ import SpatialDataPointsSource from '../src/models/VPointsSource.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const projectRoot = join(__dirname, '../../..'); -const writerRoot = join(projectRoot, 'python/spatialdata-experimental-writer'); +const writerRoot = join(projectRoot, 'python/spatialdata-js-util'); async function writeSyntheticPointsZarr(root: string) { const elementDir = join(root, 'points', 'transcripts'); @@ -52,7 +52,7 @@ PY`, ); execSync( - `uv run spatialdata-experimental-writer morton-points-from-zarr ${JSON.stringify(root)} --points-key transcripts --row-group-size 100`, + `uv run spatialdata-js-util points morton-from-zarr ${JSON.stringify(root)} --points-key transcripts --row-group-size 100`, { cwd: writerRoot, stdio: 'pipe' } ); } diff --git a/packages/core/tests/parquetFooterStats.spec.ts b/packages/core/tests/parquetFooterStats.spec.ts index fc70c5bb..9b79ea65 100644 --- a/packages/core/tests/parquetFooterStats.spec.ts +++ b/packages/core/tests/parquetFooterStats.spec.ts @@ -11,7 +11,7 @@ import { } from '../src/parquetFooterStats.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const writerRoot = join(__dirname, '../../../python/spatialdata-experimental-writer'); +const writerRoot = join(__dirname, '../../../python/spatialdata-js-util'); /** Slice the Thrift `FileMetaData` bytes out of a full parquet file. */ function footerMetaData(fileBytes: Uint8Array): Uint8Array { diff --git a/packages/core/tests/pointsFeatures.spec.ts b/packages/core/tests/pointsFeatures.spec.ts index 69ef2dc4..592d1dee 100644 --- a/packages/core/tests/pointsFeatures.spec.ts +++ b/packages/core/tests/pointsFeatures.spec.ts @@ -8,7 +8,7 @@ import SpatialDataPointsSource from '../src/models/VPointsSource.js'; import * as pointsWorkerClient from '../src/workers/pointsWorkerClient.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const writerRoot = join(__dirname, '../../../python/spatialdata-experimental-writer'); +const writerRoot = join(__dirname, '../../../python/spatialdata-js-util'); async function writePointsFeatureFixture(root: string) { const elementDir = join(root, 'points', 'transcripts'); diff --git a/packages/core/tests/vtableDirectoryResponse.spec.ts b/packages/core/tests/vtableDirectoryResponse.spec.ts index c44d9f89..6c13efa3 100644 --- a/packages/core/tests/vtableDirectoryResponse.spec.ts +++ b/packages/core/tests/vtableDirectoryResponse.spec.ts @@ -21,7 +21,7 @@ import SpatialDataTableSource from '../src/models/VTableSource.js'; * the directory path does (return null vs. throw), and both must find the parts. */ const __dirname = dirname(fileURLToPath(import.meta.url)); -const writerRoot = join(__dirname, '../../../python/spatialdata-experimental-writer'); +const writerRoot = join(__dirname, '../../../python/spatialdata-js-util'); async function writeMultipartParquetFixture(root: string, partRows: [number, number]) { execSync( diff --git a/packages/core/tests/vtableMultipart.spec.ts b/packages/core/tests/vtableMultipart.spec.ts index ae297195..21ba2b57 100644 --- a/packages/core/tests/vtableMultipart.spec.ts +++ b/packages/core/tests/vtableMultipart.spec.ts @@ -7,7 +7,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import SpatialDataTableSource from '../src/models/VTableSource.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const writerRoot = join(__dirname, '../../../python/spatialdata-experimental-writer'); +const writerRoot = join(__dirname, '../../../python/spatialdata-js-util'); async function writeMultipartParquetFixture(root: string, partRows: [number, number]) { execSync( diff --git a/python/spatialdata-codec-writer/README.md b/python/spatialdata-codec-writer/README.md deleted file mode 100644 index b68b14a9..00000000 --- a/python/spatialdata-codec-writer/README.md +++ /dev/null @@ -1,218 +0,0 @@ -# spatialdata-codec-writer - -Publishable Python tool for recompressing SpatialData/OME-Zarr image stores with -optional browser-targeted codecs (JPEG 2000 and experimental HTJ2K, which is computationally cheaper). - -This is a working prototype that may be used for experimenting with writing non-standard -stores that can as of this writing only be interpreted by the `spatialdata.js` runtime. -It is hoped that if found to be of benefit, these codecs can be adopted more broadly. -We may also review other options for comparison. - -The package vendors OpenJPH WASM and runs it through a pool of persistent Node.js -encoder workers. **Runtime requirements:** Python 3.12+, Node.js on PATH, and -`imagecodecs` for JP2K encode/decode validation. - -Codec test fixture generation lives in this repository under -`python/spatialdata-codec-writer/scripts/` (not exported from the installed wheel). - -## Install - -```bash -pip install spatialdata-codec-writer -``` - -For local development in this monorepo: - -```bash -pnpm test:codec-writer -``` - -(The vendored `vendor/openjph/` assets are gitignored; the script vendors them -from `openjph-wasm` before pytest.) - -Before building or running HTJ2K tests manually: - -```bash -node scripts/vendor-openjph-for-python.mjs -uv run --directory python/spatialdata-codec-writer pytest -``` - -## Recompress an existing SpatialData store - -Use `recompress_spatialdata` or the `recompress` CLI when you want to preserve a -whole SpatialData object and rewrite selected raster payloads. Path sources are -copied first, so tables, shapes, points, root `spatialdata_attrs`, and -unconfigured rasters are preserved without loading the full object. - -```bash -uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer recompress input.sdata.zarr output-jp2k.zarr --image-key morphology_focus --preset balanced --chunks auto --overwrite -uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer recompress input.sdata.zarr output-htj2k.zarr --image-key morphology_focus --codec experimental.openjph_htj2k --preset balanced --chunks auto --overwrite -``` - -To recompress **every** image with the same settings, omit `--image-key`; CLI flags update -`default_image` and apply to all image groups in the store: - -```bash -uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer recompress \ - input.sdata.zarr output-htj2k.zarr \ - --codec experimental.openjph_htj2k \ - --quality 0.0005 \ - --chunks auto \ - --sibling \ - --overwrite -``` - -With `--sibling`, each original image is kept and a compressed sibling is added alongside it -(e.g. `morphology_focus:htj2k_q0.0005`, `he_image:htj2k_q0.0005`). - -Parallel encoding defaults to one worker per CPU (`--workers N` to override). - -### Custom HTJ2K `quality` (instead of `--preset`) - -Presets are shortcuts. For per-dataset tuning, pass an explicit OpenJPH quantization -factor. **Lower `quality` = higher fidelity and larger output** (not JP2K-style 0–100). - -```bash -uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer recompress \ - input.sdata.zarr output-htj2k.zarr \ - --image-key morphology_focus \ - --codec experimental.openjph_htj2k \ - --quality 0.001 \ - --chunks auto \ - --sibling \ - --overwrite -``` - -With `--sibling`, the new image is named from the quality, e.g. -`morphology_focus:htj2k_q0.001`. - -Equivalent JSON config: - -```json -{ - "images": { - "morphology_focus": { - "codec": "experimental.openjph_htj2k", - "quality": 0.001, - "chunks": "auto" - } - } -} -``` - -For repeatable runs with multiple images, prefer a JSON config file: - -```json -{ - "default_image": { "codec": "imagecodecs_jpeg2k", "preset": "lossless", "chunks": "auto" }, - "images": { - "morphology_focus": { "preset": "balanced" }, - "he_image": { "preset": "small" }, - "fast_preview": { - "codec": "experimental.openjph_htj2k", - "preset": "lossless", - "chunks": "auto" - } - }, - "default_labels": { "codec": "blosc", "clevel": 5 } -} -``` - -```bash -uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer recompress input.sdata.zarr output-jp2k.zarr --config recompress.json --overwrite -``` - -JP2K presets: - -- `lossless`: reversible JP2K with exact round-trip validation. -- `balanced`: near-lossless JP2K (`level=100`). -- `small`: more compact JP2K (`level=75`). - -HTJ2K encode uses vendored OpenJPH WASM (`experimental.openjph_htj2k`, -`encoder: openjph-wasm` in manifests). Presets call -`HTJ2KEncoder.setQuality(reversible, quality)`: - -- `lossless`: `reversible=True`. -- `balanced`: `reversible=False`, `quality=0.0002`. -- `small`: `reversible=False`, `quality=0.001`. - -See [`docs/htj2k-wasm-encode-design.md`](docs/htj2k-wasm-encode-design.md) for -calibration notes. - -Labels are written with Blosc/zstd by default. Browser-targeted image codecs -support `uint8`, `int8`, `uint16`, and `int16` only. - -The recompressor writes a sidecar manifest beside the output with the expanded -config, per-raster metadata, encoded byte counts, package versions, and -representative decoded checksums. Tool provenance lives in the manifest; source -store root `spatialdata_attrs` are preserved from the copy. - -## Inspect manifests - -```bash -uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer inspect output-jp2k.manifest.json -``` - -## Generate codec fixtures (monorepo dev only) - -Fixture generation is **not** part of the published CLI. From the repository root: - -```bash -pnpm test:fixtures:generate:codecs -``` - -Or directly: - -```bash -node scripts/vendor-openjph-for-python.mjs -uv run --directory python/spatialdata-codec-writer python scripts/generate_codec_fixtures.py --output-dir ../../test-fixtures/codecs --experimental-htj2k --overwrite -``` - -Dev fixtures stamp root metadata with `experimental_codec_writer` (not a fake -`spatialdata` library version) to signal they are codec test artifacts. See -`docs/docs/vis/codec-fixtures.mdx` for browser demo instructions and a note on -provenance standardization. - -### Manual multi-dimensional volumes - -Default generated fixtures use shape `[1, 1, 1, H, W]`. For ad-hoc synthetic stores, use -`write-synthetic`: - -```bash -# From the monorepo root (pnpm forwards extra args after --) -pnpm write-synthetic -- ~/data/spatialdata/sdata_inputs/mandelbulb.zarr \ - --pattern mandelbulb --size 1024 --z 128 --t 1 --c 1 \ - --image-key mandelbulb --chunk-spatial 256 --overwrite - -# Or directly via uv -node scripts/vendor-openjph-for-python.mjs -uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer-write-synthetic \ - ~/path/out.zarr --pattern indexed --size 64 --t 2 --z 3 --codec imagecodecs_jpeg2k -``` - -Patterns: `mandelbulb` (default), `indexed`, `mandelbrot` (single 2D plane), `fractal` -(legacy fixture shape). HTJ2K is the default codec; use `--preset balanced` or -`--quality 0.001` for lossy output. - -Richer, biologically motivated or compression-aware synthesis is planned -possibly as future JS-side tooling rather than growing this Python module further. - -## Python API - -```python -from spatialdata_codec_writer import recompress_spatialdata - -result = recompress_spatialdata( - "input.sdata.zarr", - "output-jp2k.zarr", - image_key="morphology_focus", - preset="balanced", - chunks="auto", - overwrite=True, - workers=4, -) -print(result.manifest_path) -``` - -TypeScript callers can use `encodeHtj2kPlane()` from `zarrextra` directly. -See [htj2k-wasm-encode-design.md](docs/htj2k-wasm-encode-design.md). diff --git a/python/spatialdata-codec-writer/pyproject.toml b/python/spatialdata-codec-writer/pyproject.toml deleted file mode 100644 index 48bead4f..00000000 --- a/python/spatialdata-codec-writer/pyproject.toml +++ /dev/null @@ -1,46 +0,0 @@ -[project] -name = "spatialdata-codec-writer" -version = "0.1.0" -description = "Recompress SpatialData/OME-Zarr image stores with optional JP2K and HTJ2K codecs" -requires-python = ">=3.12" -readme = "README.md" -license = { text = "MIT" } -authors = [{ name = "SpatialData.ts contributors" }] -dependencies = [ - "dask[array]>=2025.0", - "imagecodecs>=2025.8.2", - "numcodecs>=0.16", - "numpy>=2.0", - "ome-zarr>=0.13", - "spatialdata>=0.7.2", - "zarr>=3.1", -] - -[project.scripts] -spatialdata-codec-writer = "spatialdata_codec_writer.cli:main" -spatialdata-codec-writer-write-synthetic = "spatialdata_codec_writer.synthetic_cli:main" - -[build-system] -requires = ["setuptools>=68"] -build-backend = "setuptools.build_meta" - -[tool.setuptools.packages.find] -where = ["src"] - -[tool.setuptools.package-data] -spatialdata_codec_writer = ["vendor/openjph/*", "vendor/encode-plane.mjs"] - -[tool.uv] -package = true - -[tool.pytest.ini_options] -testpaths = ["tests"] - -[tool.basedpyright] -extraPaths = ["scripts"] - -[dependency-groups] -dev = [ - "pytest>=8.0", -] - diff --git a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/__init__.py b/python/spatialdata-codec-writer/src/spatialdata_codec_writer/__init__.py deleted file mode 100644 index 69e02756..00000000 --- a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -from .recompress import ( - HTJ2K_PRESETS, - JP2K_PRESETS, - RecompressedSpatialData, - recompress_spatialdata, - resolve_recompression_config, -) -from .codecs import ( - CODEC_HTJ2K_LEGACY, - CODEC_HTJ2K_OPENJPH, - CODEC_JPEG2K, - HTJ2K_ENCODER, - is_htj2k_codec, -) -from .htj2k_encode import htj2k_encode_available - -__all__ = [ - "CODEC_HTJ2K_LEGACY", - "CODEC_HTJ2K_OPENJPH", - "CODEC_JPEG2K", - "HTJ2K_PRESETS", - "HTJ2K_ENCODER", - "JP2K_PRESETS", - "RecompressedSpatialData", - "htj2k_encode_available", - "is_htj2k_codec", - "recompress_spatialdata", - "resolve_recompression_config", -] diff --git a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/cli.py b/python/spatialdata-codec-writer/src/spatialdata_codec_writer/cli.py deleted file mode 100644 index 02376d66..00000000 --- a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/cli.py +++ /dev/null @@ -1,137 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import os -from pathlib import Path - -from .recompress import recompress_spatialdata - - -def _recompress_chunks(value: list[str] | None): - if value is None: - return None - if len(value) == 1 and value[0] == "auto": - return "auto" - try: - return tuple(int(part) for part in value) - except ValueError as exc: - raise argparse.ArgumentTypeError("chunks must be 'auto' or integer axis sizes") from exc - - -def _positive_int(value: str) -> int: - parsed = int(value) - if parsed <= 0: - raise argparse.ArgumentTypeError("--workers must be a positive integer") - return parsed - - -def _recompress(args: argparse.Namespace) -> None: - if args.quality is not None and args.reversible: - raise SystemExit("error: --quality cannot be used with --reversible") - if args.quality is not None and args.codec != "experimental.openjph_htj2k": - raise SystemExit( - "error: --quality is for HTJ2K only; use --codec experimental.openjph_htj2k" - ) - - result = recompress_spatialdata( - args.source, - args.dest, - config=args.config, - overwrite=args.overwrite, - image_key=args.image_key, - codec=args.codec, - preset=args.preset, - chunks=_recompress_chunks(args.chunks), - quality=args.quality, - reversible=True if args.reversible else None, - sibling=args.sibling, - workers=args.workers, - ) - print(json.dumps(result.manifest, indent=2, sort_keys=True)) - - -def _inspect(args: argparse.Namespace) -> None: - manifest_path = Path(args.path) - if manifest_path.is_dir(): - manifest_path = manifest_path.with_suffix(".manifest.json") - print(manifest_path.read_text()) - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description="Recompress SpatialData/OME-Zarr image stores with optional codecs" - ) - subparsers = parser.add_subparsers(dest="command", required=True) - - recompress = subparsers.add_parser("recompress") - recompress.add_argument("source", help="Existing SpatialData Zarr store") - recompress.add_argument("dest", help="Output SpatialData Zarr store") - recompress.add_argument("--config", help="JSON recompression config") - recompress.add_argument( - "--image-key", - help="Apply convenience flags to one image only (default: all images)", - ) - recompress.add_argument( - "--codec", - choices=["imagecodecs_jpeg2k", "experimental.openjph_htj2k"], - help="Image codec (all images unless --image-key is set)", - ) - recompress.add_argument( - "--preset", - choices=["lossless", "balanced", "small"], - help="Named image preset (ignored when --quality is set)", - ) - recompress.add_argument( - "--quality", - type=float, - metavar="Q", - help=( - "HTJ2K quantization factor (lower = better fidelity, larger output). " - "Implies lossy encoding; use with --codec experimental.openjph_htj2k. " - "Overrides preset quality." - ), - ) - recompress.add_argument( - "--reversible", - action="store_true", - help="Force lossless HTJ2K (cannot be combined with --quality)", - ) - recompress.add_argument( - "--chunks", - nargs="+", - metavar="CHUNK", - help="Use 'auto' or pass one integer per raster axis", - ) - recompress.add_argument("--overwrite", action="store_true") - recompress.add_argument( - "--sibling", - action="store_true", - help=( - "Write compressed images as new sibling groups (e.g. morphology_focus:jp2k_lossless) " - "instead of replacing the originals in-place" - ), - ) - recompress.add_argument( - "--workers", - type=_positive_int, - default=os.cpu_count() or 1, - help="Parallel encoder workers (default: CPU count)", - ) - recompress.set_defaults(func=_recompress) - - inspect = subparsers.add_parser("inspect") - inspect.add_argument("path") - inspect.set_defaults(func=_inspect) - - return parser - - -def main(argv: list[str] | None = None) -> None: - parser = build_parser() - args = parser.parse_args(argv) - args.func(args) - - -if __name__ == "__main__": - main() diff --git a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/codecs.py b/python/spatialdata-codec-writer/src/spatialdata_codec_writer/codecs.py deleted file mode 100644 index 1331a6e5..00000000 --- a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/codecs.py +++ /dev/null @@ -1,172 +0,0 @@ -from __future__ import annotations - -import hashlib -import json -from importlib import metadata -from pathlib import Path -from typing import Any, Literal - -# `imagecodecs` is used only for the JPEG2000 (`imagecodecs_jpeg2k`) path. The -# HTJ2K path goes through the vendored openjph-wasm worker (the same WASM the JS -# reader uses), so it round-trips genuine multi-component (volumetric) data and -# stays consistent encode-vs-decode. We deliberately do not maintain extra HTJ2K -# backends here: native `imagecodecs` HTJ2K is build-fragile, and `itkwasm-htj2k` -# (https://pypi.org/project/itkwasm-htj2k/) has been unmaintained for ~2 years. -# If we ever drop the JPEG2000 fixture path, `imagecodecs` can be removed too. -import imagecodecs -import numpy as np - -CODEC_JPEG2K = "imagecodecs_jpeg2k" -CODEC_HTJ2K_OPENJPH = "experimental.openjph_htj2k" -# Legacy label for stores written before OpenJPH WASM became the supported encoder. -CODEC_HTJ2K_LEGACY = "experimental.imagecodecs_htj2k" -HTJ2K_CODECS = frozenset({CODEC_HTJ2K_OPENJPH, CODEC_HTJ2K_LEGACY}) -HTJ2K_ENCODER = "openjph-wasm" - -CodecName = Literal["imagecodecs_jpeg2k", "experimental.openjph_htj2k"] - - -def is_htj2k_codec(codec: str) -> bool: - return codec in HTJ2K_CODECS - - -def package_version(name: str) -> str | None: - try: - return metadata.version(name) - except metadata.PackageNotFoundError: - return None - - -def json_bytes(value: dict[str, Any]) -> bytes: - return json.dumps(value, indent=2, sort_keys=True).encode("utf-8") - - -def write_json(path: Path, value: dict[str, Any]) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(json_bytes(value)) - - -def sha256(data: bytes | bytearray) -> str: - return hashlib.sha256(data).hexdigest() - - -def decode_htj2k_plane(encoded: bytes | bytearray) -> np.ndarray: - """Decode an HTJ2K codestream via the vendored openjph-wasm worker. - - Returns a 2D ``(y, x)`` array for single-component codestreams, otherwise a - ``(components, y, x)`` array. - """ - from .htj2k_encode import decode_htj2k - - array = decode_htj2k(encoded) - return array[0] if array.shape[0] == 1 else array - - -def decode_image_plane(encoded: bytes | bytearray, codec: str) -> np.ndarray: - if codec == CODEC_JPEG2K: - return imagecodecs.jpeg2k_decode(encoded) - if is_htj2k_codec(codec): - return decode_htj2k_plane(encoded) - raise ValueError(f"Unsupported image codec: {codec}") - - -def _htj2k_components(array: np.ndarray) -> int: - return int(np.prod(array.shape[:-2])) if array.ndim > 2 else 1 - - -def _encode_htj2k_with_options( - volume: np.ndarray, encode_options: dict[str, Any] | None = None -) -> bytes | bytearray: - from .htj2k_encode import encode_htj2k, htj2k_encode_available, openjph_encode_options - - if not htj2k_encode_available(): - raise RuntimeError( - "HTJ2K encode is not available. Install spatialdata-codec-writer with Node.js on PATH." - ) - reversible, quality = openjph_encode_options(encode_options or {}) - return encode_htj2k(volume, reversible=reversible, quality=quality) - - -def encode_image_plane( - plane: np.ndarray, codec: str, encode_options: dict[str, Any] -) -> bytes | bytearray: - array = np.asarray(plane) - if codec == CODEC_JPEG2K: - return imagecodecs.jpeg2k_encode(array, **encode_options) - if codec == CODEC_HTJ2K_OPENJPH: - return _encode_htj2k_with_options(array, encode_options) - raise ValueError(f"Unsupported image codec: {codec}") - - -def encode_image_chunk( - volume: np.ndarray, codec: str, encode_options: dict[str, Any] -) -> bytes | bytearray: - """Encode a chunk of one or more planar components to a single codestream. - - ``volume`` is ``(components, y, x)`` (or 2D for a single component). HTJ2K - encodes the planes as codestream components (e.g. z-planes of a volumetric - chunk); multi-component JPEG2K chunks are not produced by this writer. - """ - array = np.ascontiguousarray(np.asarray(volume)) - components = _htj2k_components(array) - if codec == CODEC_HTJ2K_OPENJPH: - return _encode_htj2k_with_options(array, encode_options) - if codec == CODEC_JPEG2K: - if components == 1: - plane = array.reshape(array.shape[-2], array.shape[-1]) - return imagecodecs.jpeg2k_encode(plane, **encode_options) - raise NotImplementedError( - "Multi-component JPEG2K chunks are not supported; use HTJ2K (openjph)." - ) - raise ValueError(f"Unsupported image codec: {codec}") - - -def decode_image_chunk( - encoded: bytes | bytearray, codec: str, *, components: int, height: int, width: int -) -> np.ndarray: - """Decode a chunk codestream to a ``(components, y, x)`` array.""" - if is_htj2k_codec(codec): - from .htj2k_encode import decode_htj2k - - return decode_htj2k(encoded).reshape(components, height, width) - if codec == CODEC_JPEG2K: - decoded = np.asarray(imagecodecs.jpeg2k_decode(encoded)) - if decoded.ndim == 2: - return decoded.reshape(1, height, width) - return np.moveaxis(decoded, -1, 0).reshape(components, height, width) - raise ValueError(f"Unsupported image codec: {codec}") - - -def chunk_grid(shape: tuple[int, ...], chunks: tuple[int, ...]) -> list[tuple[int, ...]]: - if len(shape) != len(chunks): - raise ValueError( - f"shape and chunks must have the same length (got {len(shape)} and {len(chunks)})" - ) - ranges = [range((size + chunk - 1) // chunk) for size, chunk in zip(shape, chunks)] - out: list[tuple[int, ...]] = [()] - for values in ranges: - out = [(*prefix, value) for prefix in out for value in values] - return out - - -def chunk_slices( - shape: tuple[int, ...], chunks: tuple[int, ...], coords: tuple[int, ...] -) -> tuple[slice, ...]: - if not (len(shape) == len(chunks) == len(coords)): - raise ValueError( - "shape, chunks, and coords must have the same length " - f"(got {len(shape)}, {len(chunks)}, and {len(coords)})" - ) - slices = [] - for coord, chunk, size in zip(coords, chunks, shape): - start = coord * chunk - slices.append(slice(start, min(start + chunk, size))) - return tuple(slices) - - -def pad_chunk(chunk: np.ndarray, chunks: tuple[int, ...]) -> np.ndarray: - if chunk.shape == chunks: - return chunk - padded = np.zeros(chunks, dtype=chunk.dtype) - padded[tuple(slice(0, size) for size in chunk.shape)] = chunk - return padded diff --git a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/synthetic_cli.py b/python/spatialdata-codec-writer/src/spatialdata_codec_writer/synthetic_cli.py deleted file mode 100644 index c294b16e..00000000 --- a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/synthetic_cli.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Console entry for repo-local synthetic image writer scripts.""" - -from __future__ import annotations - -import sys -from pathlib import Path - - -def main(argv: list[str] | None = None) -> None: - scripts_dir = Path(__file__).resolve().parents[2] / "scripts" - scripts_path = str(scripts_dir) - if scripts_path not in sys.path: - sys.path.insert(0, scripts_path) - - from write_synthetic import main as write_synthetic_main - - write_synthetic_main(argv) diff --git a/python/spatialdata-experimental-writer/README.md b/python/spatialdata-experimental-writer/README.md deleted file mode 100644 index f7aef368..00000000 --- a/python/spatialdata-experimental-writer/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# spatialdata-experimental-writer - -Experimental vector optimization writers for browser-oriented SpatialData -rendering. - -The initial writer targets Vitessce-compatible Morton-sorted Points Parquet: - -- `x`, `y`, optional `z` coordinates are preserved. -- `morton_code_2d` is added using 16 bits per axis. -- the first 2–4 rows are sentinel/extreme rows with `morton_code_2d == 0`; - readers can infer the full point bounding box from these rows. -- `{feature_key}_codes` (for example `feature_name_codes`) are added when - `feature_key` is set in element attrs. -- string/categorical columns are placed at the right side of the table. -- row-group size is controlled when writing Parquet. -- intermediate Morton uint columns are not persisted in the output Parquet. - -Morton v1 belongs on the **canonical** element path -`points//points.parquet`. Use `--experimental` only for layouts that -standard readers cannot consume (see -[ADR 0002](../../docs/adr/0002-spatially-aware-vector-loading.md)). - -Feature / gene filtering in the browser is documented in ADR 0002; pass integer -`featureCodes` through `@spatialdata/core` `loadPointsInBounds()` and -`PointsLayerConfig.featureCodes` in `@spatialdata/vis`. - -## Install - -```bash -cd python/spatialdata-experimental-writer -uv sync -``` - -For the interactive TUI: - -```bash -uv sync --group tui -``` - -## Interactive TUI - -```bash -uv run spatialdata-experimental-writer tui -uv run spatialdata-experimental-writer tui ~/data/xenium_rep1_io.zarr -``` - -The TUI wraps all writer commands: - -1. Pick a command from the home menu. -2. Enter paths and options on guided forms (Zarr store path is pre-filled when - passed on the command line). -3. Confirm before any in-place overwrite of canonical `points//points.parquet`. -4. Watch run output, then review post-write verification checks. - -Morton verification checks after Morton writes: - -| Check | Meaning | -|-------|---------| -| `column_present` | `morton_code_2d` column exists | -| `sentinel_prefix` | First 2–4 rows have `morton_code_2d == 0` | -| `sentinel_bbox` | Sentinel rows encode full dataset x/y bounds | -| `morton_monotonic` | Morton codes non-decreasing after sentinels | -| `row_group_sentinels` | Row group 0 contains only sentinel rows | -| `no_uint_intermediates` | No persisted `*_uint` staging columns | - -Multiscale and index-permutation runs show schema/manifest checks instead. - -## Commands - -```bash -# List Points elements in a store -uv run spatialdata-experimental-writer list-points ~/data/xenium.zarr - -# Morton-sort transcripts in-place on canonical points//points.parquet -uv run spatialdata-experimental-writer morton-points-from-zarr \ - ~/data/xenium.zarr --points-key transcripts - -# Optional: write to points.experimental/ instead of canonical path -uv run spatialdata-experimental-writer morton-points-from-zarr \ - ~/data/xenium.zarr --points-key transcripts --experimental - -# Build a derivative store with transcript index sort permutations -uv run spatialdata-experimental-writer write-index-permutations \ - ~/data/xenium_rep1_io.zarr \ - ~/data/xenium_rep1_index-permutations.zarr - -# Morton-sort a CSV or Parquet file -uv run spatialdata-experimental-writer morton-points input.csv output.parquet \ - --feature-key feature_name -``` - -## Xenium workflow - -Standard sandbox datasets are listed in the -[spatialdata datasets docs](https://spatialdata.scverse.org/en/stable/tutorials/notebooks/datasets/README.html): - -| Dataset | URL | -|---------|-----| -| `xenium_rep1_io.zarr` | `https://s3.embl.de/spatialdata/spatialdata-sandbox/xenium_rep1_io.zarr/` | -| `xenium_rep2_io.zarr` | `https://s3.embl.de/spatialdata/spatialdata-sandbox/xenium_rep2_io.zarr/` | -| `visium_associated_xenium_io.zarr` | `https://s3.embl.de/spatialdata/spatialdata-sandbox/visium_associated_xenium_io.zarr/` | - -After downloading a store locally: - -```bash -uv run spatialdata-experimental-writer morton-points-from-zarr \ - ~/data/spatialdata/sdata_inputs/xenium_rep1_io.zarr \ - --points-key transcripts -``` - -This replaces `points/transcripts/points.parquet` in place (single-file output; -multipart source directories are replaced). Open the store in `@spatialdata/vis` -with `experimentalOptimizations="auto"` to use TileLayer row-group reads. - -For sort-strategy benchmarks on a **copy** of the store: - -```bash -uv run spatialdata-experimental-writer write-index-permutations \ - ~/data/spatialdata/sdata_inputs/xenium_rep1_io.zarr \ - ~/data/spatialdata/sdata_inputs/xenium_rep1_index-permutations.zarr \ - --max-rows 500000 - -uv run python scripts/benchmark_points_index.py \ - ~/data/spatialdata/sdata_inputs/xenium_rep1_index-permutations.zarr -``` - -## Multiscale hook - -The package also includes a Padua-style multiscale Parquet writer that stores -`spatialdata_multiscale` JSON metadata in the Parquet schema. That layout is -non-standard for morton-points v1 and belongs under `points.experimental/` if -persisted. diff --git a/python/spatialdata-experimental-writer/pyproject.toml b/python/spatialdata-experimental-writer/pyproject.toml deleted file mode 100644 index d9303edb..00000000 --- a/python/spatialdata-experimental-writer/pyproject.toml +++ /dev/null @@ -1,37 +0,0 @@ -[project] -name = "spatialdata-experimental-writer" -version = "0.1.0" -description = "Experimental SpatialData vector optimization writers" -requires-python = ">=3.12" -readme = "README.md" -license = { text = "MIT" } -authors = [{ name = "SpatialData.js contributors" }] -dependencies = [ - "numpy>=2.0", - "pandas>=2.2", - "pyarrow>=18", -] - -[project.scripts] -spatialdata-experimental-writer = "spatialdata_experimental_writer.cli:main" - -[build-system] -requires = ["setuptools>=68"] -build-backend = "setuptools.build_meta" - -[tool.setuptools.packages.find] -where = ["src"] - -[tool.uv] -package = true - -[tool.pytest.ini_options] -testpaths = ["tests"] - -[dependency-groups] -dev = [ - "pytest>=8.0", -] -tui = [ - "textual>=1.0", -] diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/__init__.py b/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/__init__.py deleted file mode 100644 index 99eaa590..00000000 --- a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from .points import ( - MORTON_CODE_2D_COLUMN, - MORTON_CODE_EXTREME_VALUE_INDICATOR, - build_spatialdata_multiscale_metadata, - morton_sort_points, - write_morton_points_parquet, - write_multiscale_points_parquet, -) - -__all__ = [ - "MORTON_CODE_2D_COLUMN", - "MORTON_CODE_EXTREME_VALUE_INDICATOR", - "build_spatialdata_multiscale_metadata", - "morton_sort_points", - "write_morton_points_parquet", - "write_multiscale_points_parquet", -] diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/cli.py b/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/cli.py deleted file mode 100644 index 75347ece..00000000 --- a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/cli.py +++ /dev/null @@ -1,348 +0,0 @@ -from __future__ import annotations - -import argparse -import json -from collections.abc import Callable -from typing import Any - -from .errors import WriterCommandError -from .runners import ( - run_list_points, - run_morton_points, - run_morton_points_from_zarr, - run_multiscale_points, - run_write_index_permutations, -) - -_EPILOG = """\ -examples: - # List Points elements in a SpatialData Zarr store - spatialdata-experimental-writer list-points ~/data/xenium.zarr - - # Morton-sort transcripts in-place on the canonical points element - spatialdata-experimental-writer morton-points-from-zarr \\ - ~/data/xenium.zarr --points-key transcripts - - # Build a derivative store with transcript index permutations - spatialdata-experimental-writer write-index-permutations \\ - ~/data/xenium_rep1_io.zarr ~/data/xenium_rep1_index-permutations.zarr - - # Morton-sort a CSV or single Parquet file - spatialdata-experimental-writer morton-points input.csv output.parquet \\ - --feature-key feature_name - - # Write multiscale Parquet with embedded spatialdata_multiscale metadata - spatialdata-experimental-writer multiscale-points input.parquet output.parquet - - # Interactive workflow TUI - uv sync --group tui - spatialdata-experimental-writer tui ~/data/xenium.zarr -""" - - -def _positive_int(value: str) -> int: - parsed = int(value) - if parsed <= 0: - raise argparse.ArgumentTypeError("value must be positive") - return parsed - - -def _print_json(payload: dict[str, Any]) -> None: - print(json.dumps(payload, indent=2, sort_keys=True)) - - -def _run_command(command: Callable[[], dict[str, Any]]) -> None: - try: - _print_json(command()) - except WriterCommandError as exc: - raise SystemExit(str(exc)) from exc - - -def _list_points(args: argparse.Namespace) -> None: - _run_command(lambda: run_list_points(args.zarr)) - - -def _morton_points(args: argparse.Namespace) -> None: - _run_command( - lambda: run_morton_points( - args.input, - args.output, - feature_key=args.feature_key, - row_group_size=args.row_group_size, - compression=args.compression, - ) - ) - - -def _multiscale_points(args: argparse.Namespace) -> None: - _run_command( - lambda: run_multiscale_points( - args.input, - args.output, - metadata_json=args.metadata_json, - row_group_size=args.row_group_size, - compression=args.compression, - ) - ) - - -def _morton_points_from_zarr(args: argparse.Namespace) -> None: - _run_command( - lambda: run_morton_points_from_zarr( - args.zarr, - points_key=args.points_key, - experimental=args.experimental, - output=args.output, - output_points_key=args.output_points_key, - feature_key=args.feature_key, - overwrite=args.overwrite, - row_group_size=args.row_group_size, - compression=args.compression, - ) - ) - - -def _write_index_permutations(args: argparse.Namespace) -> None: - condition_ids = args.conditions.split(",") if args.conditions else None - _run_command( - lambda: run_write_index_permutations( - args.source_zarr, - args.dest_zarr, - points_key=args.points_key, - max_rows=args.max_rows, - condition_ids=condition_ids, - overwrite=args.overwrite, - row_group_size=args.row_group_size, - compression=args.compression, - ) - ) - - -def _tui(args: argparse.Namespace) -> None: - try: - from .tui.app import run_tui - except ImportError as exc: - raise SystemExit( - "TUI dependencies are not installed. Run: uv sync --group tui" - ) from exc - run_tui(initial_zarr=args.zarr) - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description=( - "Write browser-oriented SpatialData vector optimization artifacts " - "(Morton-sorted Points Parquet and multiscale metadata)." - ), - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=_EPILOG, - ) - subparsers = parser.add_subparsers(dest="command", required=True, metavar="command") - - list_points = subparsers.add_parser( - "list-points", - help="list Points element keys in a SpatialData Zarr store", - description="List Points element keys under /points/.", - ) - list_points.add_argument( - "zarr", - metavar="ZARR", - help="Path to a SpatialData Zarr store (directory containing points/)", - ) - list_points.set_defaults(func=_list_points) - - morton_from_zarr = subparsers.add_parser( - "morton-points-from-zarr", - help="Morton-sort a Points element from a SpatialData Zarr store", - description=( - "Read points//points.parquet from a SpatialData Zarr store, " - "add morton_code_2d sentinel rows, and write Vitessce-compatible Parquet. " - "Defaults to in-place replacement of points//points.parquet." - ), - ) - morton_from_zarr.add_argument( - "zarr", - metavar="ZARR", - help="Path to a SpatialData Zarr store", - ) - morton_from_zarr.add_argument( - "--experimental", - action="store_true", - help="Write to points.experimental//points.parquet instead of canonical path", - ) - morton_from_zarr.add_argument( - "--points-key", - metavar="KEY", - help=( - "Points element name under points/ (for example transcripts). " - "Required when the store has more than one Points element." - ), - ) - morton_from_zarr.add_argument( - "--output", - metavar="PATH", - help=( - "Output Parquet path (default: in-place on points//points.parquet, " - "or points.experimental//points.parquet with --experimental)" - ), - ) - morton_from_zarr.add_argument( - "--output-points-key", - metavar="KEY", - help=( - "Output Points element name under points/ (for example transcripts_morton). " - "Cannot be combined with --output." - ), - ) - morton_from_zarr.add_argument( - "--overwrite", - action="store_true", - help="Allow overwriting an existing explicit output path or output Points element.", - ) - morton_from_zarr.add_argument( - "--feature-key", - metavar="COLUMN", - help=( - "Column used to derive _codes (default: spatialdata_attrs.feature_key " - "from the element zarr.json)" - ), - ) - morton_from_zarr.add_argument( - "--row-group-size", - type=_positive_int, - default=50_000, - metavar="N", - help="Target row-group size after sentinel rows (default: 50000)", - ) - morton_from_zarr.add_argument( - "--compression", - default="zstd", - help="Parquet compression codec (default: zstd)", - ) - morton_from_zarr.set_defaults(func=_morton_points_from_zarr) - - morton = subparsers.add_parser( - "morton-points", - help="Morton-sort points from CSV or Parquet", - description=( - "Sort x/y points by 2D Morton order, prepend sentinel bbox rows, " - "and write Vitessce-compatible Parquet." - ), - ) - morton.add_argument( - "input", - metavar="INPUT", - help="Input .csv, .parquet file, or directory of Parquet parts", - ) - morton.add_argument( - "output", - metavar="OUTPUT", - help="Output .parquet file", - ) - morton.add_argument( - "--feature-key", - metavar="COLUMN", - help="Column used to derive _codes for categorical features", - ) - morton.add_argument( - "--row-group-size", - type=_positive_int, - default=50_000, - metavar="N", - help="Target row-group size after sentinel rows (default: 50000)", - ) - morton.add_argument( - "--compression", - default="zstd", - help="Parquet compression codec (default: zstd)", - ) - morton.set_defaults(func=_morton_points) - - multiscale = subparsers.add_parser( - "multiscale-points", - help="write multiscale Points Parquet with spatialdata_multiscale metadata", - description=( - "Write Points Parquet with Padua-style spatialdata_multiscale JSON " - "stored in the file schema metadata." - ), - ) - multiscale.add_argument( - "input", - metavar="INPUT", - help="Input .csv, .parquet file, or directory of Parquet parts", - ) - multiscale.add_argument( - "output", - metavar="OUTPUT", - help="Output .parquet file", - ) - multiscale.add_argument( - "--metadata-json", - metavar="PATH", - help="Optional spatialdata_multiscale metadata JSON (default: inferred from input)", - ) - multiscale.add_argument( - "--row-group-size", - type=_positive_int, - default=50_000, - metavar="N", - help="Target row-group size (default: 50000)", - ) - multiscale.add_argument( - "--compression", - default="zstd", - help="Parquet compression codec (default: zstd)", - ) - multiscale.set_defaults(func=_multiscale_points) - - index_permutations = subparsers.add_parser( - "write-index-permutations", - help="write derivative Zarr with transcript index sort permutations", - description=( - "Copy a SpatialData Zarr store and add sibling points elements with " - "different transcript sort/index layouts plus index-manifest.json." - ), - ) - index_permutations.add_argument("source_zarr", metavar="SOURCE_ZARR") - index_permutations.add_argument("dest_zarr", metavar="DEST_ZARR") - index_permutations.add_argument("--points-key", metavar="KEY") - index_permutations.add_argument("--max-rows", type=_positive_int, metavar="N") - index_permutations.add_argument( - "--conditions", - metavar="IDS", - help="Comma-separated condition ids (default: all)", - ) - index_permutations.add_argument("--overwrite", action="store_true") - index_permutations.add_argument( - "--row-group-size", - type=_positive_int, - default=50_000, - metavar="N", - ) - index_permutations.add_argument("--compression", default="zstd") - index_permutations.set_defaults(func=_write_index_permutations) - - tui = subparsers.add_parser( - "tui", - help="interactive terminal workflow for writer commands", - description="Launch a Textual workflow UI for SpatialData experimental writer commands.", - ) - tui.add_argument( - "zarr", - nargs="?", - metavar="ZARR", - help="Optional SpatialData Zarr store path (skips initial store picker)", - ) - tui.set_defaults(func=_tui) - - return parser - - -def main(argv: list[str] | None = None) -> None: - parser = build_parser() - args = parser.parse_args(argv) - args.func(args) - - -if __name__ == "__main__": - main() diff --git a/python/spatialdata-experimental-writer/uv.lock b/python/spatialdata-experimental-writer/uv.lock deleted file mode 100644 index 17728657..00000000 --- a/python/spatialdata-experimental-writer/uv.lock +++ /dev/null @@ -1,394 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.12" -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -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 = "linkify-it-py" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "uc-micro-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, -] - -[package.optional-dependencies] -linkify = [ - { name = "linkify-it-py" }, -] - -[[package]] -name = "mdit-py-plugins" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "numpy" -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/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, - { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, - { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, - { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, - { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, - { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, - { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, - { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, - { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, - { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, - { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, - { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, - { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, - { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, - { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, - { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, - { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, - { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, - { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, - { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, - { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, - { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, - { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, - { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, - { 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/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, - { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, -] - -[[package]] -name = "packaging" -version = "26.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, -] - -[[package]] -name = "pandas" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "python-dateutil" }, - { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, -] -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/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, - { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, - { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, - { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, - { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, - { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, - { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, - { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, - { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, - { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, - { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, - { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, - { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, - { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, - { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, - { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, - { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, - { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pyarrow" -version = "24.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, - { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, - { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, - { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, - { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, - { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, - { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, - { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, - { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, - { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, - { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, - { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, - { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, - { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, - { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, - { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, - { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, - { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } -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 = "pytest" -version = "9.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "rich" -version = "15.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "spatialdata-experimental-writer" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "numpy" }, - { name = "pandas" }, - { name = "pyarrow" }, -] - -[package.dev-dependencies] -dev = [ - { name = "pytest" }, -] -tui = [ - { name = "textual" }, -] - -[package.metadata] -requires-dist = [ - { name = "numpy", specifier = ">=2.0" }, - { name = "pandas", specifier = ">=2.2" }, - { name = "pyarrow", specifier = ">=18" }, -] - -[package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=8.0" }] -tui = [{ name = "textual", specifier = ">=1.0" }] - -[[package]] -name = "textual" -version = "8.2.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py", extra = ["linkify"] }, - { name = "mdit-py-plugins" }, - { name = "platformdirs" }, - { name = "pygments" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/7a/c519db0aba5024f86e71e9631810bfdd6866ed2c8695bd7fa34b90e7ef59/textual-8.2.7.tar.gz", hash = "sha256:658f568ff81e30ed43890c3e07520390e5cf1b4763822006e060656b0a88f105", size = 1859249, upload-time = "2026-05-19T10:52:49.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/f5/c1e18bc0707300a0e90204343abbf7d7acd6fb7ebe03a6d4893b99a234b8/textual-8.2.7-py3-none-any.whl", hash = "sha256:4caaa13a90bc4cf9c6c862c067ccd34fe84e9c161710a2a907a8026313b6bd73", size = 731129, upload-time = "2026-05-19T10:52:51.773Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "tzdata" -version = "2026.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, -] - -[[package]] -name = "uc-micro-py" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, -] diff --git a/python/spatialdata-codec-writer/docs/htj2k-wasm-encode-design.md b/python/spatialdata-js-util/docs/htj2k-wasm-encode-design.md similarity index 100% rename from python/spatialdata-codec-writer/docs/htj2k-wasm-encode-design.md rename to python/spatialdata-js-util/docs/htj2k-wasm-encode-design.md diff --git a/python/spatialdata-codec-writer/docs/multi-component-codec-findings.md b/python/spatialdata-js-util/docs/multi-component-codec-findings.md similarity index 100% rename from python/spatialdata-codec-writer/docs/multi-component-codec-findings.md rename to python/spatialdata-js-util/docs/multi-component-codec-findings.md diff --git a/python/spatialdata-js-util/pyproject.toml b/python/spatialdata-js-util/pyproject.toml new file mode 100644 index 00000000..3bde1619 --- /dev/null +++ b/python/spatialdata-js-util/pyproject.toml @@ -0,0 +1,83 @@ +[project] +name = "spatialdata-js-util" +version = "0.1.0" +description = "Read/write utilities for browser-oriented SpatialData stores (image codecs, Morton-indexed points, CSC tables)" +requires-python = ">=3.12" +readme = "README.md" +license = { text = "MIT" } +authors = [{ name = "SpatialData.js contributors" }] +keywords = ["spatialdata", "zarr", "ome-zarr", "htj2k", "jpeg2000", "spatial-omics"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Bio-Informatics", +] +dependencies = [ + "imagecodecs>=2025.8.2", + "numcodecs>=0.16", + "numpy>=2.0", + "pandas>=2.2", + "pyarrow>=18", + "zarr>=3.1", +] + +# The base install is deliberately light enough to serve as a *reader* shim: it +# registers the image codecs with zarr and nothing here pulls in spatialdata +# itself. Writing pulls in the heavier stack. +[project.optional-dependencies] +write = [ + "anndata>=0.12", + "dask[array]>=2025.0", + "ome-zarr>=0.16", + "scipy>=1.11", + "spatialdata>=0.8.0", +] +tui = ["textual>=1.0"] +all = ["spatialdata-js-util[write,tui]"] + +[project.urls] +Homepage = "https://github.com/Taylor-CCB-Group/SpatialData.js" +Repository = "https://github.com/Taylor-CCB-Group/SpatialData.js" + +[project.scripts] +spatialdata-js-util = "spatialdata_js_util.cli:main" + +# Registers our image codecs with zarr-python v3 so that `zarr.open` — and +# therefore `spatialdata.read_zarr` — can decode stores this package writes. +# Merely installing the distribution is enough; no import is required. +[project.entry-points."zarr.codecs"] +"experimental.openjph_htj2k" = "spatialdata_js_util.codecs.zarr_codec:Htj2kCodec" +"experimental.imagecodecs_htj2k" = "spatialdata_js_util.codecs.zarr_codec:LegacyHtj2kCodec" +"imagecodecs_jpeg2k" = "spatialdata_js_util.codecs.zarr_codec:Jpeg2kCodec" + +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +spatialdata_js_util = [ + "codecs/probe/*.j2c", + "codecs/probe/*.npy", + "codecs/vendor/*.mjs", + "codecs/vendor/openjph/*", + "codecs/vendor/openjph/wasm/*", +] + +[tool.uv] +package = true + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.basedpyright] +extraPaths = ["scripts"] + +[dependency-groups] +dev = [ + "pytest>=8.0", +] diff --git a/python/spatialdata-experimental-writer/scripts/benchmark_points_index.py b/python/spatialdata-js-util/scripts/benchmark_points_index.py similarity index 100% rename from python/spatialdata-experimental-writer/scripts/benchmark_points_index.py rename to python/spatialdata-js-util/scripts/benchmark_points_index.py diff --git a/python/spatialdata-js-util/scripts/build_htj2k_probe.py b/python/spatialdata-js-util/scripts/build_htj2k_probe.py new file mode 100644 index 00000000..b4c0a4c3 --- /dev/null +++ b/python/spatialdata-js-util/scripts/build_htj2k_probe.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Regenerate the committed multi-component HTJ2K probe fixture. + +The fixture gates whether an HTJ2K backend may be used (see +`spatialdata_js_util.codecs.backends`). It is deliberately produced by the +**openjph-wasm** backend, so passing the probe means "this backend can read what +our writer produces", not merely "this backend is self-consistent". + +Requires Node.js and the vendored WASM assets: + + node scripts/vendor-openjph-for-python.mjs + uv run --directory python/spatialdata-js-util python scripts/build_htj2k_probe.py +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np + +PROBE_DIR = Path(__file__).resolve().parent.parent / "src/spatialdata_js_util/codecs/probe" + +# Small, but with enough structure that a component mix-up cannot pass by luck: +# every plane has a distinct gradient and a distinct constant offset. +COMPONENTS, HEIGHT, WIDTH = 3, 16, 12 + + +def probe_volume() -> np.ndarray: + y, x = np.mgrid[0:HEIGHT, 0:WIDTH] + planes = [ + (x * 7 + y * 13 * (c + 1) + c * 1000 + 1).astype(np.uint16) for c in range(COMPONENTS) + ] + return np.ascontiguousarray(np.stack(planes, axis=0)) + + +def main() -> int: + from spatialdata_js_util.codecs.htj2k_wasm import ( + decode_htj2k_wasm, + encode_htj2k_wasm, + wasm_encode_available, + ) + + if not wasm_encode_available(): + print( + "openjph-wasm backend unavailable. Ensure Node.js is on PATH and run\n" + " node scripts/vendor-openjph-for-python.mjs", + file=sys.stderr, + ) + return 1 + + volume = probe_volume() + codestream = encode_htj2k_wasm(volume, reversible=True) + + roundtrip = decode_htj2k_wasm(codestream).reshape(volume.shape) + if not np.array_equal(roundtrip, volume): + print("Refusing to write a probe fixture the encoder cannot round-trip.", file=sys.stderr) + return 1 + + PROBE_DIR.mkdir(parents=True, exist_ok=True) + (PROBE_DIR / "multicomponent.j2c").write_bytes(codestream) + np.save(PROBE_DIR / "multicomponent.npy", volume) + print( + f"Wrote probe fixture to {PROBE_DIR}: " + f"{len(codestream)} byte codestream, {volume.shape} {volume.dtype} expected samples" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/python/spatialdata-codec-writer/scripts/fixture_writer.py b/python/spatialdata-js-util/scripts/fixture_writer.py similarity index 98% rename from python/spatialdata-codec-writer/scripts/fixture_writer.py rename to python/spatialdata-js-util/scripts/fixture_writer.py index 8074f791..f70cf8f2 100644 --- a/python/spatialdata-codec-writer/scripts/fixture_writer.py +++ b/python/spatialdata-js-util/scripts/fixture_writer.py @@ -7,7 +7,7 @@ import numpy as np -from spatialdata_codec_writer.codecs import ( +from spatialdata_js_util.codecs import ( CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K, HTJ2K_ENCODER, @@ -15,12 +15,11 @@ chunk_grid, decode_image_chunk, encode_image_chunk, + htj2k_encode_options, is_htj2k_codec, - package_version, - sha256, - write_json, ) -from spatialdata_codec_writer.htj2k_encode import openjph_encode_options +from spatialdata_js_util.provenance import package_version, sha256 +from spatialdata_js_util.store import write_json from provenance import experimental_codec_writer_attrs @@ -201,9 +200,7 @@ def image_to_tczyx(image: Any, dims: tuple[str, ...] | list[str] | None = None) def _is_lossless_encode(codec: str, encode_options: dict[str, Any] | None) -> bool: if is_htj2k_codec(codec): - from spatialdata_codec_writer.htj2k_encode import openjph_encode_options - - reversible, _quality = openjph_encode_options(encode_options or {}) + reversible, _quality = htj2k_encode_options(encode_options or {}) return reversible if codec == CODEC_JPEG2K: return bool((encode_options or {}).get("reversible", True)) diff --git a/python/spatialdata-codec-writer/scripts/generate_codec_fixtures.py b/python/spatialdata-js-util/scripts/generate_codec_fixtures.py similarity index 96% rename from python/spatialdata-codec-writer/scripts/generate_codec_fixtures.py rename to python/spatialdata-js-util/scripts/generate_codec_fixtures.py index 274a0524..09e6194f 100644 --- a/python/spatialdata-codec-writer/scripts/generate_codec_fixtures.py +++ b/python/spatialdata-js-util/scripts/generate_codec_fixtures.py @@ -15,7 +15,7 @@ if str(_SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(_SCRIPTS_DIR)) -from spatialdata_codec_writer.htj2k_encode import htj2k_encode_available +from spatialdata_js_util.codecs import htj2k_available from fixture_writer import write_jpeg2k_fixture from htj2k_fixtures import ( @@ -38,7 +38,7 @@ def _generate_fixtures(args: argparse.Namespace) -> None: written = [write_jpeg2k_fixture(output_dir / "jpeg2k.zarr", overwrite=args.overwrite)] if args.experimental_htj2k: - if htj2k_encode_available(): + if htj2k_available(): written.append( write_mandelbulb_fixture(output_dir / "mandelbulb.zarr", overwrite=args.overwrite) ) diff --git a/python/spatialdata-codec-writer/scripts/htj2k_fixtures.py b/python/spatialdata-js-util/scripts/htj2k_fixtures.py similarity index 96% rename from python/spatialdata-codec-writer/scripts/htj2k_fixtures.py rename to python/spatialdata-js-util/scripts/htj2k_fixtures.py index 4f7d25f7..fd4e16f4 100644 --- a/python/spatialdata-codec-writer/scripts/htj2k_fixtures.py +++ b/python/spatialdata-js-util/scripts/htj2k_fixtures.py @@ -5,13 +5,14 @@ import numpy as np -from spatialdata_codec_writer.codecs import ( +from spatialdata_js_util.codecs import ( CODEC_HTJ2K_OPENJPH, HTJ2K_ENCODER, decode_htj2k_plane, - write_json, + htj2k_available, ) -from spatialdata_codec_writer.htj2k_encode import encode_htj2k_plane, htj2k_encode_available +from spatialdata_js_util.codecs.htj2k_wasm import encode_htj2k_wasm +from spatialdata_js_util.store import write_json from fixture_writer import CodecImageWrite, WrittenFixture, write_codec_spatialdata, write_codec_spatialdata_images from synthetic_images import fractal_tczyx_image, mandelbrot_plane @@ -151,14 +152,14 @@ def _plane_error_metrics(source: np.ndarray, decoded: np.ndarray) -> dict[str, f def write_htj2k_quality_sweep_manifest(path: str | Path) -> Path: - if not htj2k_encode_available(): + if not htj2k_available(): raise RuntimeError("OpenJPH WASM HTJ2K encoder is not available.") plane = mandelbrot_plane(64) raw_bytes = int(plane.nbytes) qualities: list[dict[str, Any]] = [] for entry in HTJ2K_QUALITY_SWEEP: - encoded = encode_htj2k_plane( + encoded = encode_htj2k_wasm( plane, reversible=bool(entry["reversible"]), quality=float(entry["quality"]), diff --git a/python/spatialdata-codec-writer/scripts/mandelbulb_fixtures.py b/python/spatialdata-js-util/scripts/mandelbulb_fixtures.py similarity index 95% rename from python/spatialdata-codec-writer/scripts/mandelbulb_fixtures.py rename to python/spatialdata-js-util/scripts/mandelbulb_fixtures.py index e9b34939..6ed91a3c 100644 --- a/python/spatialdata-codec-writer/scripts/mandelbulb_fixtures.py +++ b/python/spatialdata-js-util/scripts/mandelbulb_fixtures.py @@ -2,7 +2,7 @@ from pathlib import Path -from spatialdata_codec_writer.codecs import CODEC_HTJ2K_OPENJPH +from spatialdata_js_util.codecs import CODEC_HTJ2K_OPENJPH from fixture_writer import WrittenFixture, write_codec_spatialdata from synthetic_images import volume_tczyx diff --git a/python/spatialdata-codec-writer/scripts/provenance.py b/python/spatialdata-js-util/scripts/provenance.py similarity index 100% rename from python/spatialdata-codec-writer/scripts/provenance.py rename to python/spatialdata-js-util/scripts/provenance.py diff --git a/python/spatialdata-codec-writer/scripts/synthetic_images.py b/python/spatialdata-js-util/scripts/synthetic_images.py similarity index 100% rename from python/spatialdata-codec-writer/scripts/synthetic_images.py rename to python/spatialdata-js-util/scripts/synthetic_images.py diff --git a/python/spatialdata-codec-writer/scripts/write_synthetic.py b/python/spatialdata-js-util/scripts/write_synthetic.py similarity index 96% rename from python/spatialdata-codec-writer/scripts/write_synthetic.py rename to python/spatialdata-js-util/scripts/write_synthetic.py index 2d4d6711..3ef66900 100644 --- a/python/spatialdata-codec-writer/scripts/write_synthetic.py +++ b/python/spatialdata-js-util/scripts/write_synthetic.py @@ -16,8 +16,8 @@ if str(_SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(_SCRIPTS_DIR)) -from spatialdata_codec_writer.codecs import CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K, CodecName -from spatialdata_codec_writer.htj2k_encode import htj2k_encode_available +from spatialdata_js_util.codecs import CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K, CodecName +from spatialdata_js_util.codecs import htj2k_available from fixture_writer import WrittenFixture, write_codec_spatialdata from synthetic_images import fractal_tczyx_image, mandelbrot_plane, volume_tczyx @@ -74,7 +74,7 @@ def _encode_options(args: argparse.Namespace) -> dict[str, Any] | None: def _write(args: argparse.Namespace) -> WrittenFixture: - if args.codec == CODEC_HTJ2K_OPENJPH and not htj2k_encode_available(): + if args.codec == CODEC_HTJ2K_OPENJPH and not htj2k_available(): raise SystemExit( "HTJ2K encode is not available. Install Node.js on PATH and vendor OpenJPH " "(node scripts/vendor-openjph-for-python.mjs from the monorepo root)." diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/__init__.py b/python/spatialdata-js-util/src/spatialdata_js_util/__init__.py new file mode 100644 index 00000000..bf332491 --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/__init__.py @@ -0,0 +1,73 @@ +"""Utilities for reading and writing browser-oriented SpatialData stores. + +Three kinds of optimization live here, all aimed at making a SpatialData store +cheap to read incrementally over HTTP from `SpatialData.js`: + +* **images** — recompress rasters with JPEG 2000 or HTJ2K so tiles decode in the + browser (`recompress_spatialdata`). +* **points** — Morton-sort Points Parquet with sentinel bounding-box rows and + controlled row groups, so a viewport maps to a few row-group reads + (`write_morton_points_parquet`). +* **tables** — convert AnnData matrices to CSC so reading one gene is one + contiguous range read (`convert_store_tables_to_csc`). + +Installing this distribution also registers the image codecs with zarr-python, +so `spatialdata.read_zarr` can open stores written here — see +`spatialdata_js_util.codecs.zarr_codec`. +""" + +from .codecs import ( + CODEC_HTJ2K_LEGACY, + CODEC_HTJ2K_OPENJPH, + CODEC_JPEG2K, + backend_report, + htj2k_available, + is_htj2k_codec, + register_codecs, +) +from .images import ( + HTJ2K_PRESETS, + JP2K_PRESETS, + RecompressedSpatialData, + recompress_spatialdata, + resolve_recompression_config, +) +from .points import ( + MORTON_CODE_2D_COLUMN, + MORTON_CODE_EXTREME_VALUE_INDICATOR, + build_spatialdata_multiscale_metadata, + morton_sort_points, + write_morton_points_parquet, + write_multiscale_points_parquet, +) +from .tables import ( + CscConversion, + anndata_to_csc, + convert_store_tables_to_csc, + to_csc, +) + +__all__ = [ + "CODEC_HTJ2K_LEGACY", + "CODEC_HTJ2K_OPENJPH", + "CODEC_JPEG2K", + "HTJ2K_PRESETS", + "JP2K_PRESETS", + "MORTON_CODE_2D_COLUMN", + "MORTON_CODE_EXTREME_VALUE_INDICATOR", + "CscConversion", + "RecompressedSpatialData", + "anndata_to_csc", + "backend_report", + "build_spatialdata_multiscale_metadata", + "convert_store_tables_to_csc", + "htj2k_available", + "is_htj2k_codec", + "morton_sort_points", + "recompress_spatialdata", + "register_codecs", + "resolve_recompression_config", + "to_csc", + "write_morton_points_parquet", + "write_multiscale_points_parquet", +] diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/cli.py b/python/spatialdata-js-util/src/spatialdata_js_util/cli.py new file mode 100644 index 00000000..8751cff0 --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/cli.py @@ -0,0 +1,565 @@ +"""Unified `spatialdata-js-util` command line. + +Subcommands are grouped by what they act on: + + images recompress rasters with browser-readable codecs + points Morton-index Points elements + tables convert table matrices to CSC + codecs inspect codec backend availability + tui interactive workflow UI +""" + +from __future__ import annotations + +import argparse +import json +import os +from collections.abc import Callable +from pathlib import Path +from typing import Any + +from .errors import WriterCommandError + +_EPILOG = """\ +examples: + # Recompress every image in a store as lossy HTJ2K siblings + spatialdata-js-util images recompress in.zarr out.zarr \\ + --codec experimental.openjph_htj2k --quality 0.0005 --sibling --overwrite + + # Morton-sort transcripts in place + spatialdata-js-util points morton-from-zarr ~/data/xenium.zarr --points-key transcripts + + # Convert table matrices to CSC for fast per-gene reads in the browser + spatialdata-js-util tables to-csc ~/data/xenium.zarr + + # Show which HTJ2K backend is in use + spatialdata-js-util codecs info +""" + + +def _positive_int(value: str) -> int: + parsed = int(value) + if parsed <= 0: + raise argparse.ArgumentTypeError("value must be positive") + return parsed + + +def _print_json(payload: dict[str, Any]) -> None: + print(json.dumps(payload, indent=2, sort_keys=True)) + + +def _run_command(command: Callable[[], dict[str, Any]]) -> None: + try: + _print_json(command()) + except WriterCommandError as exc: + raise SystemExit(str(exc)) from exc + + +# --------------------------------------------------------------------------- images + + +def _recompress_chunks(value: list[str] | None): + if value is None: + return None + if len(value) == 1 and value[0] == "auto": + return "auto" + try: + return tuple(int(part) for part in value) + except ValueError as exc: + raise argparse.ArgumentTypeError("chunks must be 'auto' or integer axis sizes") from exc + + +def _images_recompress(args: argparse.Namespace) -> None: + from .images import recompress_spatialdata + + if args.quality is not None and args.reversible: + raise SystemExit("error: --quality cannot be used with --reversible") + if args.quality is not None and args.codec != "experimental.openjph_htj2k": + raise SystemExit( + "error: --quality is for HTJ2K only; use --codec experimental.openjph_htj2k" + ) + + result = recompress_spatialdata( + args.source, + args.dest, + config=args.config, + overwrite=args.overwrite, + image_key=args.image_key, + codec=args.codec, + preset=args.preset, + chunks=_recompress_chunks(args.chunks), + quality=args.quality, + reversible=True if args.reversible else None, + sibling=args.sibling, + workers=args.workers, + ) + _print_json(result.manifest) + + +def _images_inspect(args: argparse.Namespace) -> None: + manifest_path = Path(args.path) + if manifest_path.is_dir(): + manifest_path = manifest_path.with_suffix(".manifest.json") + print(manifest_path.read_text()) + + +def _add_images_commands(subparsers: argparse._SubParsersAction) -> None: + images = subparsers.add_parser( + "images", + help="recompress rasters with browser-readable codecs", + description="Recompress SpatialData/OME-Zarr image stores.", + ) + image_commands = images.add_subparsers(dest="images_command", required=True, metavar="command") + + recompress = image_commands.add_parser( + "recompress", + help="rewrite image rasters with JPEG 2000 or HTJ2K", + description=( + "Copy a SpatialData store and recompress configured image rasters. " + "Tables, shapes, points, and unconfigured rasters are preserved." + ), + ) + recompress.add_argument("source", help="Existing SpatialData Zarr store") + recompress.add_argument("dest", help="Output SpatialData Zarr store") + recompress.add_argument("--config", help="JSON recompression config") + recompress.add_argument( + "--image-key", + help="Apply convenience flags to one image only (default: all images)", + ) + recompress.add_argument( + "--codec", + choices=["imagecodecs_jpeg2k", "experimental.openjph_htj2k"], + help="Image codec (all images unless --image-key is set)", + ) + recompress.add_argument( + "--preset", + choices=["lossless", "balanced", "small"], + help="Named image preset (ignored when --quality is set)", + ) + recompress.add_argument( + "--quality", + type=float, + metavar="Q", + help=( + "HTJ2K quantization factor (lower = better fidelity, larger output). " + "Implies lossy encoding; use with --codec experimental.openjph_htj2k. " + "Overrides preset quality." + ), + ) + recompress.add_argument( + "--reversible", + action="store_true", + help="Force lossless HTJ2K (cannot be combined with --quality)", + ) + recompress.add_argument( + "--chunks", + nargs="+", + metavar="CHUNK", + help="Use 'auto' or pass one integer per raster axis", + ) + recompress.add_argument("--overwrite", action="store_true") + recompress.add_argument( + "--sibling", + action="store_true", + help=( + "Write compressed images as new sibling groups (e.g. morphology_focus:jp2k_lossless) " + "instead of replacing the originals in-place" + ), + ) + recompress.add_argument( + "--workers", + type=_positive_int, + default=os.cpu_count() or 1, + help="Parallel encoder workers (default: CPU count)", + ) + recompress.set_defaults(func=_images_recompress) + + inspect = image_commands.add_parser( + "inspect", + help="print a recompression manifest", + ) + inspect.add_argument("path") + inspect.set_defaults(func=_images_inspect) + + +# --------------------------------------------------------------------------- points + + +def _points_list(args: argparse.Namespace) -> None: + from .runners import run_list_points + + _run_command(lambda: run_list_points(args.zarr)) + + +def _points_morton(args: argparse.Namespace) -> None: + from .runners import run_morton_points + + _run_command( + lambda: run_morton_points( + args.input, + args.output, + feature_key=args.feature_key, + row_group_size=args.row_group_size, + compression=args.compression, + ) + ) + + +def _points_multiscale(args: argparse.Namespace) -> None: + from .runners import run_multiscale_points + + _run_command( + lambda: run_multiscale_points( + args.input, + args.output, + metadata_json=args.metadata_json, + row_group_size=args.row_group_size, + compression=args.compression, + ) + ) + + +def _points_morton_from_zarr(args: argparse.Namespace) -> None: + from .runners import run_morton_points_from_zarr + + _run_command( + lambda: run_morton_points_from_zarr( + args.zarr, + points_key=args.points_key, + experimental=args.experimental, + output=args.output, + output_points_key=args.output_points_key, + feature_key=args.feature_key, + overwrite=args.overwrite, + row_group_size=args.row_group_size, + compression=args.compression, + ) + ) + + +def _points_index_permutations(args: argparse.Namespace) -> None: + from .runners import run_write_index_permutations + + condition_ids = args.conditions.split(",") if args.conditions else None + _run_command( + lambda: run_write_index_permutations( + args.source_zarr, + args.dest_zarr, + points_key=args.points_key, + max_rows=args.max_rows, + condition_ids=condition_ids, + overwrite=args.overwrite, + row_group_size=args.row_group_size, + compression=args.compression, + ) + ) + + +def _add_row_group_args(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--row-group-size", + type=_positive_int, + default=50_000, + metavar="N", + help="Target row-group size after sentinel rows (default: 50000)", + ) + parser.add_argument( + "--compression", + default="zstd", + help="Parquet compression codec (default: zstd)", + ) + + +def _add_points_commands(subparsers: argparse._SubParsersAction) -> None: + points = subparsers.add_parser( + "points", + help="Morton-index Points elements for spatial browser reads", + description=( + "Write browser-oriented Points artifacts: Morton-sorted Parquet with " + "sentinel bounding-box rows and controlled row groups." + ), + ) + point_commands = points.add_subparsers(dest="points_command", required=True, metavar="command") + + list_points = point_commands.add_parser( + "list", + help="list Points element keys in a store", + description="List Points element keys under /points/.", + ) + list_points.add_argument("zarr", metavar="ZARR", help="Path to a SpatialData Zarr store") + list_points.set_defaults(func=_points_list) + + morton_from_zarr = point_commands.add_parser( + "morton-from-zarr", + help="Morton-sort a Points element from a store", + description=( + "Read points//points.parquet from a SpatialData Zarr store, " + "add morton_code_2d sentinel rows, and write Vitessce-compatible Parquet. " + "Defaults to in-place replacement of points//points.parquet." + ), + ) + morton_from_zarr.add_argument("zarr", metavar="ZARR", help="Path to a SpatialData Zarr store") + morton_from_zarr.add_argument( + "--experimental", + action="store_true", + help="Write to points.experimental//points.parquet instead of the canonical path", + ) + morton_from_zarr.add_argument( + "--points-key", + metavar="KEY", + help=( + "Points element name under points/ (for example transcripts). " + "Required when the store has more than one Points element." + ), + ) + morton_from_zarr.add_argument( + "--output", + metavar="PATH", + help=( + "Output Parquet path (default: in-place on points//points.parquet, " + "or points.experimental//points.parquet with --experimental)" + ), + ) + morton_from_zarr.add_argument( + "--output-points-key", + metavar="KEY", + help=( + "Output Points element name under points/ (for example transcripts_morton). " + "Cannot be combined with --output." + ), + ) + morton_from_zarr.add_argument( + "--overwrite", + action="store_true", + help="Allow overwriting an existing explicit output path or output Points element.", + ) + morton_from_zarr.add_argument( + "--feature-key", + metavar="COLUMN", + help=( + "Column used to derive _codes (default: spatialdata_attrs.feature_key " + "from the element zarr.json)" + ), + ) + _add_row_group_args(morton_from_zarr) + morton_from_zarr.set_defaults(func=_points_morton_from_zarr) + + morton = point_commands.add_parser( + "morton", + help="Morton-sort points from CSV or Parquet", + description=( + "Sort x/y points by 2D Morton order, prepend sentinel bbox rows, " + "and write Vitessce-compatible Parquet." + ), + ) + morton.add_argument( + "input", metavar="INPUT", help="Input .csv, .parquet file, or directory of Parquet parts" + ) + morton.add_argument("output", metavar="OUTPUT", help="Output .parquet file") + morton.add_argument( + "--feature-key", + metavar="COLUMN", + help="Column used to derive _codes for categorical features", + ) + _add_row_group_args(morton) + morton.set_defaults(func=_points_morton) + + multiscale = point_commands.add_parser( + "multiscale", + help="write multiscale Points Parquet with spatialdata_multiscale metadata", + description=( + "Write Points Parquet with Padua-style spatialdata_multiscale JSON " + "stored in the file schema metadata." + ), + ) + multiscale.add_argument( + "input", metavar="INPUT", help="Input .csv, .parquet file, or directory of Parquet parts" + ) + multiscale.add_argument("output", metavar="OUTPUT", help="Output .parquet file") + multiscale.add_argument( + "--metadata-json", + metavar="PATH", + help="Optional spatialdata_multiscale metadata JSON (default: inferred from input)", + ) + _add_row_group_args(multiscale) + multiscale.set_defaults(func=_points_multiscale) + + index_permutations = point_commands.add_parser( + "index-permutations", + help="write a derivative store with transcript index sort permutations", + description=( + "Copy a SpatialData Zarr store and add sibling points elements with " + "different transcript sort/index layouts plus index-manifest.json." + ), + ) + index_permutations.add_argument("source_zarr", metavar="SOURCE_ZARR") + index_permutations.add_argument("dest_zarr", metavar="DEST_ZARR") + index_permutations.add_argument("--points-key", metavar="KEY") + index_permutations.add_argument("--max-rows", type=_positive_int, metavar="N") + index_permutations.add_argument( + "--conditions", metavar="IDS", help="Comma-separated condition ids (default: all)" + ) + index_permutations.add_argument("--overwrite", action="store_true") + _add_row_group_args(index_permutations) + index_permutations.set_defaults(func=_points_index_permutations) + + +# --------------------------------------------------------------------------- tables + + +def _tables_list(args: argparse.Namespace) -> None: + from .runners import run_list_tables + + _run_command(lambda: run_list_tables(args.zarr)) + + +def _tables_to_csc(args: argparse.Namespace) -> None: + from .runners import run_tables_to_csc + + _run_command( + lambda: run_tables_to_csc( + args.source, + args.dest, + tables=args.table.split(",") if args.table else None, + layers=not args.no_layers, + densify=args.densify, + overwrite=args.overwrite, + ) + ) + + +def _add_tables_commands(subparsers: argparse._SubParsersAction) -> None: + tables = subparsers.add_parser( + "tables", + help="convert table matrices to CSC", + description="Rewrite AnnData table matrices for fast per-variable browser reads.", + ) + table_commands = tables.add_subparsers(dest="tables_command", required=True, metavar="command") + + list_tables = table_commands.add_parser( + "list", help="list table element keys in a store" + ) + list_tables.add_argument("zarr", metavar="ZARR", help="Path to a SpatialData Zarr store") + list_tables.set_defaults(func=_tables_list) + + to_csc = table_commands.add_parser( + "to-csc", + help="convert X (and layers) to CSC", + description=( + "Convert table matrices from CSR to CSC so that reading one variable " + "(gene) is a single contiguous range read instead of a scan of every row. " + "Values are unchanged and the result stays readable by any AnnData client." + ), + ) + to_csc.add_argument("source", metavar="SOURCE", help="SpatialData Zarr store") + to_csc.add_argument( + "dest", + metavar="DEST", + nargs="?", + help="Output store (default: rewrite SOURCE in place)", + ) + to_csc.add_argument( + "--table", + metavar="KEYS", + help="Comma-separated table element names (default: all tables)", + ) + to_csc.add_argument( + "--no-layers", + action="store_true", + help="Convert X only, leaving adata.layers untouched", + ) + to_csc.add_argument( + "--densify", + action="store_true", + help=( + "Also convert dense matrices to CSC. Off by default because sparsifying " + "a dense matrix can make it larger." + ), + ) + to_csc.add_argument("--overwrite", action="store_true", help="Replace an existing DEST") + to_csc.set_defaults(func=_tables_to_csc) + + +# --------------------------------------------------------------------------- codecs + + +def _codecs_info(_args: argparse.Namespace) -> None: + from .codecs.backends import backend_report + + _print_json(backend_report()) + + +def _add_codecs_commands(subparsers: argparse._SubParsersAction) -> None: + codecs = subparsers.add_parser( + "codecs", + help="inspect image codec backends", + description="Report which HTJ2K backends are installed and which one is selected.", + ) + codec_commands = codecs.add_subparsers(dest="codecs_command", required=True, metavar="command") + + info = codec_commands.add_parser( + "info", + help="show HTJ2K backend availability and probe results", + description=( + "Show each HTJ2K backend's availability and whether it decodes the bundled " + "multi-component probe codestream correctly, plus which one is selected." + ), + ) + info.set_defaults(func=_codecs_info) + + +# --------------------------------------------------------------------------- tui + + +def _tui(args: argparse.Namespace) -> None: + try: + from .tui.app import run_tui + except ImportError as exc: + raise SystemExit( + "TUI dependencies are not installed. Install with: pip install 'spatialdata-js-util[tui]'" + ) from exc + run_tui(initial_zarr=args.zarr) + + +def _add_tui_command(subparsers: argparse._SubParsersAction) -> None: + tui = subparsers.add_parser( + "tui", + help="interactive terminal workflow for points commands", + description="Launch a Textual workflow UI for the Points writer commands.", + ) + tui.add_argument( + "zarr", + nargs="?", + metavar="ZARR", + help="Optional SpatialData Zarr store path (skips initial store picker)", + ) + tui.set_defaults(func=_tui) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="spatialdata-js-util", + description=( + "Read/write utilities for browser-oriented SpatialData stores: image " + "codecs, Morton-indexed points, and CSC table matrices." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=_EPILOG, + ) + subparsers = parser.add_subparsers(dest="group", required=True, metavar="group") + _add_images_commands(subparsers) + _add_points_commands(subparsers) + _add_tables_commands(subparsers) + _add_codecs_commands(subparsers) + _add_tui_command(subparsers) + return parser + + +def main(argv: list[str] | None = None) -> None: + parser = build_parser() + args = parser.parse_args(argv) + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/__init__.py b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/__init__.py new file mode 100644 index 00000000..856c3079 --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/__init__.py @@ -0,0 +1,61 @@ +"""Image codecs for browser-oriented SpatialData stores. + +`names` holds the on-disk codec identifiers, `backends` picks an HTJ2K +implementation, `encoding` does the chunk-level work, and `zarr_codec` exposes +all of it to zarr-python as registered codecs. +""" + +from .backends import ( + BACKEND_IMAGECODECS, + BACKEND_OPENJPH_WASM, + backend_report, + htj2k_available, + resolve_backend, +) +from .chunks import chunk_grid, chunk_slices, pad_chunk +from .encoding import ( + decode_htj2k_plane, + decode_image_chunk, + decode_image_plane, + encode_image_chunk, + encode_image_plane, + htj2k_encode_options, +) +from .names import ( + CODEC_HTJ2K_LEGACY, + CODEC_HTJ2K_OPENJPH, + CODEC_JPEG2K, + HTJ2K_ENCODER, + SUPPORTED_IMAGE_CODECS, + CodecName, + is_htj2k_codec, +) +from .zarr_codec import Htj2kCodec, Jpeg2kCodec, LegacyHtj2kCodec, register_codecs + +__all__ = [ + "BACKEND_IMAGECODECS", + "BACKEND_OPENJPH_WASM", + "CODEC_HTJ2K_LEGACY", + "CODEC_HTJ2K_OPENJPH", + "CODEC_JPEG2K", + "HTJ2K_ENCODER", + "SUPPORTED_IMAGE_CODECS", + "Htj2kCodec", + "Jpeg2kCodec", + "LegacyHtj2kCodec", + "CodecName", + "backend_report", + "chunk_grid", + "chunk_slices", + "decode_htj2k_plane", + "decode_image_chunk", + "decode_image_plane", + "encode_image_chunk", + "encode_image_plane", + "htj2k_available", + "htj2k_encode_options", + "is_htj2k_codec", + "pad_chunk", + "register_codecs", + "resolve_backend", +] diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/backends.py b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/backends.py new file mode 100644 index 00000000..cad502b6 --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/backends.py @@ -0,0 +1,215 @@ +"""HTJ2K backend selection. + +Two backends can produce and consume the HTJ2K codestreams this package writes: + +``imagecodecs`` + Native OpenJPH via `imagecodecs.htj2k_encode/decode`. Pure `pip install` — + no Node.js — so it is what makes the reader shim usable from a plain + SpatialData environment. + +``openjph-wasm`` + The vendored WASM build driven through a pool of Node.js worker processes. + This is the same WASM the JS reader uses, so it is the reference for + encode/decode agreement with the browser. + +Historically only the WASM backend was trusted, because the WASM decoder this +repo previously depended on silently dropped every component but the first (see +`docs/multi-component-codec-findings.md`). That defect is specific to that build, +not to OpenJPH — but "a build of OpenJPH" is not on its own evidence of +correctness. So `imagecodecs` is not trusted on reputation: it is admitted only +if it decodes a committed multi-component codestream, produced by the WASM +encoder, to the exact expected samples. The probe runs once per process and the +backend is rejected on any mismatch. + +Set ``SPATIALDATA_JS_UTIL_HTJ2K_BACKEND=imagecodecs|openjph-wasm`` to pin one +backend explicitly (the probe still gates ``imagecodecs``). +""" + +from __future__ import annotations + +import os +from functools import lru_cache +from pathlib import Path +from typing import Any, Protocol + +import numpy as np + +BACKEND_IMAGECODECS = "imagecodecs" +BACKEND_OPENJPH_WASM = "openjph-wasm" + +_BACKEND_ENV_VAR = "SPATIALDATA_JS_UTIL_HTJ2K_BACKEND" +_PROBE_DIR = Path(__file__).resolve().parent / "probe" +_PROBE_CODESTREAM = _PROBE_DIR / "multicomponent.j2c" +_PROBE_EXPECTED = _PROBE_DIR / "multicomponent.npy" + + +class Htj2kBackend(Protocol): + """Encode/decode HTJ2K codestreams of planar ``(components, y, x)`` volumes.""" + + name: str + + def available(self) -> bool: ... + + def encode(self, volume: np.ndarray, *, reversible: bool, quality: float) -> bytes: ... + + def decode(self, codestream: bytes | bytearray) -> np.ndarray: ... + + +def _as_planar_3d(array: np.ndarray) -> np.ndarray: + """Normalise a decoded result to ``(components, y, x)``.""" + if array.ndim == 2: + return array.reshape(1, *array.shape) + if array.ndim == 3: + return array + raise ValueError(f"Expected a 2D or 3D HTJ2K decode result, got shape {array.shape}") + + +class ImagecodecsHtj2kBackend: + """Native OpenJPH through `imagecodecs`; no Node.js required.""" + + name = BACKEND_IMAGECODECS + + def available(self) -> bool: + try: + import imagecodecs + except ImportError: + return False + return bool(getattr(imagecodecs, "HTJ2K", None) and imagecodecs.HTJ2K.available) + + def encode(self, volume: np.ndarray, *, reversible: bool, quality: float) -> bytes: + import imagecodecs + + array = np.ascontiguousarray(np.asarray(volume)) + planar = array.ndim == 3 and array.shape[0] > 1 + if array.ndim == 3 and array.shape[0] == 1: + array = array.reshape(array.shape[1], array.shape[2]) + if reversible: + # `level` is the OpenJPH quantization step; omitting it selects the + # reversible (5/3) transform. + return bytes(imagecodecs.htj2k_encode(array, reversible=True, planar=planar, rgb=False)) + return bytes( + imagecodecs.htj2k_encode( + array, level=float(quality), reversible=False, planar=planar, rgb=False + ) + ) + + def decode(self, codestream: bytes | bytearray) -> np.ndarray: + import imagecodecs + + return _as_planar_3d(np.asarray(imagecodecs.htj2k_decode(bytes(codestream), planar=True))) + + +class OpenJphWasmHtj2kBackend: + """Vendored openjph-wasm driven by Node.js worker processes.""" + + name = BACKEND_OPENJPH_WASM + + def available(self) -> bool: + from .htj2k_wasm import wasm_encode_available + + return wasm_encode_available() + + def encode(self, volume: np.ndarray, *, reversible: bool, quality: float) -> bytes: + from .htj2k_wasm import encode_htj2k_wasm + + return encode_htj2k_wasm(volume, reversible=reversible, quality=quality) + + def decode(self, codestream: bytes | bytearray) -> np.ndarray: + from .htj2k_wasm import decode_htj2k_wasm + + return _as_planar_3d(decode_htj2k_wasm(codestream)) + + +_BACKENDS: dict[str, Htj2kBackend] = { + BACKEND_IMAGECODECS: ImagecodecsHtj2kBackend(), + BACKEND_OPENJPH_WASM: OpenJphWasmHtj2kBackend(), +} + +# `imagecodecs` first so that a plain `pip install` can both read and write +# without Node.js. The probe below is what makes that safe. +_PREFERENCE = (BACKEND_IMAGECODECS, BACKEND_OPENJPH_WASM) + + +def probe_fixture() -> tuple[bytes, np.ndarray]: + """Return the committed multi-component codestream and its expected samples.""" + if not _PROBE_CODESTREAM.is_file() or not _PROBE_EXPECTED.is_file(): + raise FileNotFoundError( + f"HTJ2K probe fixture missing under {_PROBE_DIR}. " + "Regenerate it with scripts/build_htj2k_probe.py." + ) + return _PROBE_CODESTREAM.read_bytes(), np.load(_PROBE_EXPECTED) + + +@lru_cache(maxsize=None) +def backend_passes_probe(name: str) -> bool: + """Whether *name* decodes the committed multi-component fixture exactly. + + A backend that raises, returns the wrong shape, or returns wrong samples + fails. Multi-component correctness cannot be assumed from the library name: + the WASM build previously used here decoded every component as component 0. + """ + backend = _BACKENDS.get(name) + if backend is None or not backend.available(): + return False + try: + codestream, expected = probe_fixture() + decoded = backend.decode(codestream) + except Exception: + return False + return decoded.shape == expected.shape and np.array_equal(decoded, expected) + + +@lru_cache(maxsize=1) +def resolve_backend() -> Htj2kBackend | None: + """Return the HTJ2K backend to use, or ``None`` if none is usable.""" + requested = os.environ.get(_BACKEND_ENV_VAR) + candidates = (requested,) if requested else _PREFERENCE + if requested and requested not in _BACKENDS: + raise ValueError( + f"Unknown {_BACKEND_ENV_VAR}={requested!r}; expected one of {sorted(_BACKENDS)}" + ) + for name in candidates: + # The WASM backend is the reference the probe fixture was produced with, + # so it is trusted on availability; every other backend must prove it. + if name == BACKEND_OPENJPH_WASM: + if _BACKENDS[name].available(): + return _BACKENDS[name] + elif backend_passes_probe(name): + return _BACKENDS[name] + return None + + +def reset_backend_cache() -> None: + """Clear memoized backend selection (used by tests that flip the env var).""" + resolve_backend.cache_clear() + backend_passes_probe.cache_clear() + + +def require_backend() -> Htj2kBackend: + backend = resolve_backend() + if backend is None: + raise RuntimeError( + "No usable HTJ2K backend. Install `imagecodecs` with HTJ2K support " + "(pip install 'spatialdata-js-util'), or put Node.js on PATH to use the " + "vendored openjph-wasm backend." + ) + return backend + + +def htj2k_available() -> bool: + return resolve_backend() is not None + + +def backend_report() -> dict[str, Any]: + """Describe backend availability — used by manifests and `codecs info`.""" + selected = resolve_backend() + return { + "selected": selected.name if selected else None, + "backends": { + name: { + "available": backend.available(), + "passes_multicomponent_probe": backend_passes_probe(name), + } + for name, backend in _BACKENDS.items() + }, + } diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/chunks.py b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/chunks.py new file mode 100644 index 00000000..6b12f75d --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/chunks.py @@ -0,0 +1,40 @@ +"""Chunk-grid arithmetic shared by the recompressor and its tests.""" + +from __future__ import annotations + +import numpy as np + + +def chunk_grid(shape: tuple[int, ...], chunks: tuple[int, ...]) -> list[tuple[int, ...]]: + if len(shape) != len(chunks): + raise ValueError( + f"shape and chunks must have the same length (got {len(shape)} and {len(chunks)})" + ) + ranges = [range((size + chunk - 1) // chunk) for size, chunk in zip(shape, chunks)] + out: list[tuple[int, ...]] = [()] + for values in ranges: + out = [(*prefix, value) for prefix in out for value in values] + return out + + +def chunk_slices( + shape: tuple[int, ...], chunks: tuple[int, ...], coords: tuple[int, ...] +) -> tuple[slice, ...]: + if not (len(shape) == len(chunks) == len(coords)): + raise ValueError( + "shape, chunks, and coords must have the same length " + f"(got {len(shape)}, {len(chunks)}, and {len(coords)})" + ) + slices = [] + for coord, chunk, size in zip(coords, chunks, shape): + start = coord * chunk + slices.append(slice(start, min(start + chunk, size))) + return tuple(slices) + + +def pad_chunk(chunk: np.ndarray, chunks: tuple[int, ...]) -> np.ndarray: + if chunk.shape == chunks: + return chunk + padded = np.zeros(chunks, dtype=chunk.dtype) + padded[tuple(slice(0, size) for size in chunk.shape)] = chunk + return padded diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/encoding.py b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/encoding.py new file mode 100644 index 00000000..389b1e06 --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/encoding.py @@ -0,0 +1,111 @@ +"""Encode and decode image chunks for the codecs this package supports. + +JPEG 2000 goes through `imagecodecs`. HTJ2K goes through whichever backend +`backends.resolve_backend()` selects. Both directions are expressed in terms of +planar ``(components, y, x)`` volumes; 2D planes are the single-component case. +""" + +from __future__ import annotations + +from typing import Any + +import numpy as np + +from .backends import require_backend +from .names import CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K, is_htj2k_codec + + +def htj2k_encode_options(encode_options: dict[str, Any]) -> tuple[bool, float]: + """Map encode options to the OpenJPH ``(reversible, quality)`` pair. + + ``quality`` is the OpenJPH quantization step: lower means higher fidelity and + a larger codestream. It is *not* a JPEG-style 0–100 quality. + """ + reversible = bool(encode_options.get("reversible", True)) + if reversible: + return True, 0.0 + quality = encode_options.get("quality", encode_options.get("level", 0.0002)) + return False, float(quality) + + +def _encode_htj2k(volume: np.ndarray, encode_options: dict[str, Any] | None = None) -> bytes: + reversible, quality = htj2k_encode_options(encode_options or {}) + return require_backend().encode(volume, reversible=reversible, quality=quality) + + +def _htj2k_components(array: np.ndarray) -> int: + return int(np.prod(array.shape[:-2])) if array.ndim > 2 else 1 + + +def decode_htj2k_plane(encoded: bytes | bytearray) -> np.ndarray: + """Decode an HTJ2K codestream. + + Returns a 2D ``(y, x)`` array for single-component codestreams, otherwise a + ``(components, y, x)`` array. + """ + array = require_backend().decode(encoded) + return array[0] if array.shape[0] == 1 else array + + +def decode_image_plane(encoded: bytes | bytearray, codec: str) -> np.ndarray: + if codec == CODEC_JPEG2K: + import imagecodecs + + return imagecodecs.jpeg2k_decode(encoded) + if is_htj2k_codec(codec): + return decode_htj2k_plane(encoded) + raise ValueError(f"Unsupported image codec: {codec}") + + +def encode_image_plane( + plane: np.ndarray, codec: str, encode_options: dict[str, Any] +) -> bytes | bytearray: + array = np.asarray(plane) + if codec == CODEC_JPEG2K: + import imagecodecs + + return imagecodecs.jpeg2k_encode(array, **encode_options) + if codec == CODEC_HTJ2K_OPENJPH: + return _encode_htj2k(array, encode_options) + raise ValueError(f"Unsupported image codec: {codec}") + + +def encode_image_chunk( + volume: np.ndarray, codec: str, encode_options: dict[str, Any] +) -> bytes | bytearray: + """Encode one or more planar components to a single codestream. + + ``volume`` is ``(components, y, x)`` (or 2D for a single component). HTJ2K + encodes the planes as codestream components (e.g. z-planes of a volumetric + chunk); multi-component JPEG2K chunks are not produced by this writer. + """ + array = np.ascontiguousarray(np.asarray(volume)) + components = _htj2k_components(array) + if codec == CODEC_HTJ2K_OPENJPH: + return _encode_htj2k(array, encode_options) + if codec == CODEC_JPEG2K: + import imagecodecs + + if components == 1: + plane = array.reshape(array.shape[-2], array.shape[-1]) + return imagecodecs.jpeg2k_encode(plane, **encode_options) + raise NotImplementedError( + "Multi-component JPEG2K chunks are not supported; use HTJ2K (openjph)." + ) + raise ValueError(f"Unsupported image codec: {codec}") + + +def decode_image_chunk( + encoded: bytes | bytearray, codec: str, *, components: int, height: int, width: int +) -> np.ndarray: + """Decode a chunk codestream to a ``(components, y, x)`` array.""" + if is_htj2k_codec(codec): + return require_backend().decode(encoded).reshape(components, height, width) + if codec == CODEC_JPEG2K: + import imagecodecs + + decoded = np.asarray(imagecodecs.jpeg2k_decode(encoded)) + if decoded.ndim == 2: + return decoded.reshape(1, height, width) + return np.moveaxis(decoded, -1, 0).reshape(components, height, width) + raise ValueError(f"Unsupported image codec: {codec}") diff --git a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/htj2k_encode.py b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/htj2k_wasm.py similarity index 93% rename from python/spatialdata-codec-writer/src/spatialdata_codec_writer/htj2k_encode.py rename to python/spatialdata-js-util/src/spatialdata_js_util/codecs/htj2k_wasm.py index d40b42ea..03871e79 100644 --- a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/htj2k_encode.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/htj2k_wasm.py @@ -1,3 +1,11 @@ +"""The openjph-wasm HTJ2K backend: vendored WASM driven by Node.js workers. + +This is the same WASM build the JS reader uses, which makes it the reference for +encode/decode agreement with the browser. Prefer the `backends` module over +calling this directly — it selects between this and the Node-free `imagecodecs` +backend. +""" + from __future__ import annotations import atexit @@ -233,17 +241,17 @@ def shutdown_encoder_pool() -> None: @lru_cache(maxsize=1) -def htj2k_encode_available() -> bool: - """Return whether the vendored OpenJPH WASM encode helper is usable.""" +def wasm_encode_available() -> bool: + """Return whether the vendored OpenJPH WASM helper (and Node.js) is usable.""" try: plane = np.zeros((4, 4), dtype=np.uint16) - encode_htj2k_plane(plane, reversible=True, quality=0.0) + encode_htj2k_wasm(plane, reversible=True, quality=0.0) except Exception: return False return True -def encode_htj2k( +def encode_htj2k_wasm( volume: np.ndarray, *, reversible: bool = True, @@ -253,16 +261,6 @@ def encode_htj2k( return get_encoder_pool().encode(volume, reversible=reversible, quality=quality) -def encode_htj2k_plane( - plane: np.ndarray, - *, - reversible: bool = True, - quality: float = 0.0, -) -> bytes: - """Encode one 2D plane through the vendored OpenJPH WASM worker pool.""" - return encode_htj2k(plane, reversible=reversible, quality=quality) - - -def decode_htj2k(codestream: bytes | bytearray) -> np.ndarray: +def decode_htj2k_wasm(codestream: bytes | bytearray) -> np.ndarray: """Decode an HTJ2K codestream to a (components, y, x) array via openjph-wasm.""" return get_encoder_pool().decode(codestream) diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/names.py b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/names.py new file mode 100644 index 00000000..5e2cd788 --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/names.py @@ -0,0 +1,26 @@ +"""Codec identifiers shared by the writer, the reader shim, and the JS runtime. + +These strings are the `name` field of the zarr v3 array metadata `codecs` entry, +so they are part of the on-disk contract. They must stay in sync with +`packages/zarrextra/src/codecs.ts`. +""" + +from __future__ import annotations + +from typing import Literal + +CODEC_JPEG2K = "imagecodecs_jpeg2k" +CODEC_HTJ2K_OPENJPH = "experimental.openjph_htj2k" +# Legacy label for stores written before OpenJPH became the supported encoder. +CODEC_HTJ2K_LEGACY = "experimental.imagecodecs_htj2k" + +HTJ2K_CODECS = frozenset({CODEC_HTJ2K_OPENJPH, CODEC_HTJ2K_LEGACY}) +SUPPORTED_IMAGE_CODECS = frozenset({CODEC_JPEG2K, CODEC_HTJ2K_OPENJPH}) + +HTJ2K_ENCODER = "openjph" + +CodecName = Literal["imagecodecs_jpeg2k", "experimental.openjph_htj2k"] + + +def is_htj2k_codec(codec: str) -> bool: + return codec in HTJ2K_CODECS diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/probe/multicomponent.j2c b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/probe/multicomponent.j2c new file mode 100644 index 0000000000000000000000000000000000000000..f959d034a2c79a32e2b8ac0562d0910177350cac GIT binary patch literal 423 zcmezG|38pH-+=)Lcz{>{#DD@+J~KZfBLw{qVBlb20xIJA9|Y9I#K6eF%EIEn_&tO_A;ixDUaRw2v)R&nAV6IhAh0R~|P z>0*YGUku72Kov0ln>P&3Fn$A2KI!N`hGdX{@Bs!rR(4SR6aRz|gY|Dyje# zEt&Q20EYvH@H)Ng2Lu|x!deR$L>*K-C*51Ve7RFOgM^^qAD-_g-)sjO1d>{SklHjQ i=+qwuppzNj?1ai5K*;*EB}zQ|!@%)Ppb;qj|0V$FLWN%d literal 0 HcmV?d00001 diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/probe/multicomponent.npy b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/probe/multicomponent.npy new file mode 100644 index 0000000000000000000000000000000000000000..98205a7ae65a6eec2e192d7471bdcc60cb596f6f GIT binary patch literal 1280 zcmbV|`F{^|9LHa?*W35~{*W`|-dr2zF-Dtnv}{_8IcI3B8N(Rc%(2H@nIm&#B=?a! zM{=Jb!h?{7TBwmiL=hf@*1yo}@p%65eE#^#?btmkI@Svr$n@)ak%&eN;*f}Bq+u*3Vj52+8sjkqGcgZ~u?ib72*WW7_yo(b7MrmH`*9Sfa2|880L!ojo3I`Ga0DlD4wrBh zOR*ZC;S21=VVuBOT*MXJz#VMFHtfNd_!{5fJN$_2xQz#Rg55ZT<2ZvnFNC$lqqvM&d4DASqA zNu16(T)<@mo1Wx5F&gT+-%8lH{JP(ny+0Yl)C3=_bD7FM$#)t)#8Amo5@3{bZmFlNJ&t zkrFL25+{k0ENL=UCdxGFE`20H2FnN;E#qa1%#?YuSXN1jWJs1wmKicv7Rm})CtGBv z9FT84-+O-Y{OY;qdE|NSDWayOw1QUCr=C}yqWYS?u9dZh*42g@qM;hD)wPZ`&?eeK z!!%N(HAdq!QIj=7qqLj$*7tRg4%bmSPO~*fb9JbuYo<=p={iRj=rUcSn{>PG(|NjB zSLp`*TzBgsJ+5aoPxG}v_v=wTrRVjsUelX;S0Cy#^)j{VdRrgp6Md;-#jUJWGGFtz zKr<_46|9=owg78v&Fw9F$2wULYiJ=BYT?$wx>_%bwx2?9z4%#t0ZEI|kZMS`P#7^2dyJT1G7yI4*vNM)v`Bq@J z?7ls=7e-gim2nl_LwjakPF+b?-c@zATzwbhnz=TvlJj-`F3<(LR<5mU@4C2H*Ut@f F{{jz8IxzqM literal 0 HcmV?d00001 diff --git a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/vendor/__init__.py b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/vendor/__init__.py similarity index 100% rename from python/spatialdata-codec-writer/src/spatialdata_codec_writer/vendor/__init__.py rename to python/spatialdata-js-util/src/spatialdata_js_util/codecs/vendor/__init__.py diff --git a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/vendor/encode-plane.mjs b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/vendor/encode-plane.mjs similarity index 100% rename from python/spatialdata-codec-writer/src/spatialdata_codec_writer/vendor/encode-plane.mjs rename to python/spatialdata-js-util/src/spatialdata_js_util/codecs/vendor/encode-plane.mjs diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/zarr_codec.py b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/zarr_codec.py new file mode 100644 index 00000000..e8ebaa6e --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/zarr_codec.py @@ -0,0 +1,138 @@ +"""zarr-python v3 codec shim for the image codecs this package writes. + +Registered through the ``zarr.codecs`` entry points declared in +``pyproject.toml``, so installing the distribution is enough to make +``zarr.open`` — and therefore ``spatialdata.read_zarr`` — decode stores written +by this package. No import or `register_codecs()` call is required; the explicit +`register_codecs()` below exists only for environments running from a source +tree without installed metadata. + +The stores we write put a single array->bytes codec in the array metadata:: + + "codecs": [{"name": "experimental.openjph_htj2k", "configuration": {}}] + +so each chunk file is a bare codestream covering the whole chunk. Chunks have +singleton non-spatial axes (e.g. ``[1, 1, 1, 1024, 1024]``), which is why decode +reshapes the ``(components, y, x)`` result onto the full chunk shape. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +import numpy as np +from zarr.abc.codec import ArrayBytesCodec +from zarr.core.buffer import Buffer, NDBuffer + +from .encoding import decode_image_chunk, encode_image_chunk +from .names import CODEC_HTJ2K_LEGACY, CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K + +if TYPE_CHECKING: + from zarr.core.array_spec import ArraySpec + + +def _spatial_dims(shape: tuple[int, ...]) -> tuple[int, int, int]: + """Split a chunk shape into ``(components, height, width)``. + + Leading axes are collapsed into the component count, matching the planar + ``(components, y, x)`` layout the encoders use. + """ + if len(shape) < 2: + raise ValueError(f"Image codec chunks must be at least 2D, got shape {shape}") + height, width = int(shape[-2]), int(shape[-1]) + components = int(np.prod(shape[:-2])) if len(shape) > 2 else 1 + return components, height, width + + +@dataclass(frozen=True) +class _ImageCodec(ArrayBytesCodec): + """Base for the array->bytes image codecs; subclasses set `codec_name`.""" + + #: zarr v3 codec `name`; overridden per subclass. + codec_name = "" + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "_ImageCodec": + # `configuration` is empty for these codecs but is accepted (and + # ignored) so that metadata written by other tools still loads. + return cls() + + def to_dict(self) -> dict[str, Any]: + return {"name": self.codec_name, "configuration": {}} + + def compute_encoded_size(self, input_byte_length: int, _chunk_spec: "ArraySpec") -> int: + raise NotImplementedError( + f"{self.codec_name} is a compressor; encoded size is not known ahead of time." + ) + + async def _decode_single(self, chunk_bytes: Buffer, chunk_spec: "ArraySpec") -> NDBuffer: + components, height, width = _spatial_dims(tuple(chunk_spec.shape)) + decoded = decode_image_chunk( + chunk_bytes.to_bytes(), + self.codec_name, + components=components, + height=height, + width=width, + ) + dtype = chunk_spec.dtype.to_native_dtype() + chunk = np.ascontiguousarray(decoded, dtype=dtype).reshape(tuple(chunk_spec.shape)) + return chunk_spec.prototype.nd_buffer.from_numpy_array(chunk) + + async def _encode_single(self, chunk_array: NDBuffer, chunk_spec: "ArraySpec") -> Buffer | None: + components, height, width = _spatial_dims(tuple(chunk_spec.shape)) + volume = chunk_array.as_numpy_array().reshape(components, height, width) + encoded = encode_image_chunk(volume, self.codec_name, self._encode_options()) + return chunk_spec.prototype.buffer.from_bytes(bytes(encoded)) + + def _encode_options(self) -> dict[str, Any]: + """Encode settings used when zarr writes through this codec. + + Lossless, because a codec instantiated from array metadata carries no + quality configuration — the writer in `images.py` is the supported way to + choose a lossy preset. + """ + return {"reversible": True} + + +@dataclass(frozen=True) +class Htj2kCodec(_ImageCodec): + """HTJ2K (High-Throughput JPEG 2000) via OpenJPH.""" + + codec_name = CODEC_HTJ2K_OPENJPH + + +@dataclass(frozen=True) +class LegacyHtj2kCodec(_ImageCodec): + """HTJ2K under the pre-OpenJPH label; decode-compatible with `Htj2kCodec`.""" + + codec_name = CODEC_HTJ2K_LEGACY + + +@dataclass(frozen=True) +class Jpeg2kCodec(_ImageCodec): + """JPEG 2000 via `imagecodecs`.""" + + codec_name = CODEC_JPEG2K + + def _encode_options(self) -> dict[str, Any]: + return {"reversible": True} + + +CODEC_CLASSES: dict[str, type[_ImageCodec]] = { + CODEC_HTJ2K_OPENJPH: Htj2kCodec, + CODEC_HTJ2K_LEGACY: LegacyHtj2kCodec, + CODEC_JPEG2K: Jpeg2kCodec, +} + + +def register_codecs() -> None: + """Register the image codecs with zarr explicitly. + + Installing this distribution already registers them via entry points. Call + this only when running from a source checkout with no installed metadata. + """ + from zarr.registry import register_codec + + for name, codec_cls in CODEC_CLASSES.items(): + register_codec(name, codec_cls) diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/errors.py b/python/spatialdata-js-util/src/spatialdata_js_util/errors.py similarity index 100% rename from python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/errors.py rename to python/spatialdata-js-util/src/spatialdata_js_util/errors.py diff --git a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/recompress.py b/python/spatialdata-js-util/src/spatialdata_js_util/images.py similarity index 93% rename from python/spatialdata-codec-writer/src/spatialdata_codec_writer/recompress.py rename to python/spatialdata-js-util/src/spatialdata_js_util/images.py index 1da798f2..6b0e0a05 100644 --- a/python/spatialdata-codec-writer/src/spatialdata_codec_writer/recompress.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/images.py @@ -16,23 +16,27 @@ CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K, HTJ2K_ENCODER, + SUPPORTED_IMAGE_CODECS, chunk_grid, chunk_slices, decode_image_plane, encode_image_plane, is_htj2k_codec, - package_version, pad_chunk, - sha256, - write_json, ) -from .htj2k_encode import configure_encoder_pool, htj2k_encode_available +from .codecs.backends import ( + BACKEND_OPENJPH_WASM, + backend_report, + htj2k_available, + resolve_backend, +) +from .codecs.htj2k_wasm import configure_encoder_pool +from .provenance import package_version, sha256 +from .store import read_json, refresh_consolidated_metadata, write_json ImagePreset = Literal["lossless", "balanced", "small"] ChunkSpec = Literal["auto"] | tuple[int, ...] | list[int] -SUPPORTED_IMAGE_CODECS = frozenset({CODEC_JPEG2K, CODEC_HTJ2K_OPENJPH}) - SUPPORTED_BROWSER_JP2K_DTYPES = { np.dtype("uint8"), np.dtype("int8"), @@ -60,16 +64,12 @@ class RecompressedSpatialData: manifest: dict[str, Any] -def _read_json(path: Path) -> dict[str, Any]: - return json.loads(path.read_text()) - - def _load_config(config: str | Path | dict[str, Any] | None) -> dict[str, Any]: if config is None: return {} if isinstance(config, dict): return json.loads(json.dumps(config)) - return _read_json(Path(config)) + return read_json(Path(config)) def _default_config() -> dict[str, Any]: @@ -172,7 +172,7 @@ def _attrs_from_node(path: Path) -> dict[str, Any]: meta_path = path / "zarr.json" if not meta_path.exists(): return {} - return _read_json(meta_path).get("attributes", {}) + return read_json(meta_path).get("attributes", {}) def _datasets_from_raster_group(path: Path) -> list[str]: @@ -187,7 +187,7 @@ def _datasets_from_raster_group(path: Path) -> list[str]: levels = [] for child in path.iterdir() if path.exists() else []: if child.is_dir() and (child / "zarr.json").exists(): - meta = _read_json(child / "zarr.json") + meta = read_json(child / "zarr.json") if meta.get("node_type") == "array": levels.append(child.name) return sorted(levels, key=lambda value: (not value.isdigit(), value)) @@ -236,10 +236,11 @@ def _resolve_image_codec(config: dict[str, Any], raster_key: str) -> str: raise ValueError( f"Unsupported image codec for {raster_key!r}: {codec!r}; expected one of {supported}" ) - if codec == CODEC_HTJ2K_OPENJPH and not htj2k_encode_available(): + if codec == CODEC_HTJ2K_OPENJPH and not htj2k_available(): raise RuntimeError( - f"HTJ2K recompression requested for {raster_key!r}, but the OpenJPH WASM encoder " - "is not available. Install spatialdata-codec-writer and ensure Node.js is on PATH." + f"HTJ2K recompression requested for {raster_key!r}, but no HTJ2K backend is " + "available. Install `imagecodecs` with HTJ2K support, or put Node.js on PATH " + "to use the vendored openjph-wasm backend." ) return codec @@ -372,7 +373,7 @@ def _recompress_image_array( config: dict[str, Any], workers: int, ) -> dict[str, Any]: - source_meta = _read_json(source_array_path / "zarr.json") + source_meta = read_json(source_array_path / "zarr.json") source_array = zarr.open_array(str(source_array_path), mode="r") shape = tuple(int(value) for value in source_array.shape) dtype = np.dtype(source_array.dtype) @@ -454,7 +455,9 @@ def _recompress_image_array( "chunks_checked": chunks_checked, } if is_htj2k_codec(codec): + backend = resolve_backend() report["encoder"] = HTJ2K_ENCODER + report["encoder_backend"] = backend.name if backend else None return report @@ -476,7 +479,7 @@ def _recompress_label_array( raster_path: str, config: dict[str, Any], ) -> dict[str, Any]: - source_meta = _read_json(source_array_path / "zarr.json") + source_meta = read_json(source_array_path / "zarr.json") source_array = zarr.open_array(str(source_array_path), mode="r") shape = tuple(int(value) for value in source_array.shape) dtype = np.dtype(source_array.dtype) @@ -505,7 +508,7 @@ def _recompress_label_array( dest_array[selection] = source_array[selection] chunk_count += 1 - meta = _read_json(dest_array_path / "zarr.json") + meta = read_json(dest_array_path / "zarr.json") return { "path": raster_path, "codec": "blosc", @@ -517,27 +520,6 @@ def _recompress_label_array( } -def _collect_consolidated_metadata(store_path: Path) -> dict[str, Any]: - metadata: dict[str, Any] = {} - for meta_path in store_path.rglob("zarr.json"): - if meta_path == store_path / "zarr.json": - continue - rel = meta_path.parent.relative_to(store_path).as_posix() - metadata[rel] = _read_json(meta_path) - return metadata - - -def _refresh_consolidated_metadata(store_path: Path) -> None: - root_path = store_path / "zarr.json" - root_meta = _read_json(root_path) - root_meta["consolidated_metadata"] = { - "kind": "inline", - "must_understand": False, - "metadata": _collect_consolidated_metadata(store_path), - } - write_json(root_path, root_meta) - - def _prepare_path_source(source_path: Path, dest: Path, *, overwrite: bool) -> Path: if dest.exists(): if not overwrite: @@ -593,7 +575,11 @@ def recompress_spatialdata( import os worker_count = workers if workers is not None else (os.cpu_count() or 1) - configure_encoder_pool(worker_count) + # Only the WASM backend has a worker pool to size; `imagecodecs` releases the + # GIL in its own encoder, so the ThreadPoolExecutor below is enough. + selected = resolve_backend() + if selected is not None and selected.name == BACKEND_OPENJPH_WASM: + configure_encoder_pool(worker_count) dest_path = Path(dest) temp_dir: tempfile.TemporaryDirectory[str] | None = None @@ -688,7 +674,7 @@ def recompress_spatialdata( ) ) - _refresh_consolidated_metadata(dest_path) + refresh_consolidated_metadata(dest_path) manifest_data = { "format": "spatialdata-codec-recompression/v1", @@ -698,10 +684,12 @@ def recompress_spatialdata( "images": image_reports, "labels": label_reports, "workers": worker_count, + "htj2k": backend_report(), "packages": { "imagecodecs": package_version("imagecodecs"), "numpy": package_version("numpy"), "spatialdata": package_version("spatialdata"), + "spatialdata-js-util": package_version("spatialdata-js-util"), "zarr": package_version("zarr"), }, } diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/index_permutations.py b/python/spatialdata-js-util/src/spatialdata_js_util/index_permutations.py similarity index 99% rename from python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/index_permutations.py rename to python/spatialdata-js-util/src/spatialdata_js_util/index_permutations.py index 9463044b..5e46e9ff 100644 --- a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/index_permutations.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/index_permutations.py @@ -9,7 +9,7 @@ import pandas as pd from .points import MORTON_CODE_2D_COLUMN, write_morton_points_parquet -from .zarr import ( +from .store import ( list_points_keys, points_parquet_path, read_points_dataframe, diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/points.py b/python/spatialdata-js-util/src/spatialdata_js_util/points.py similarity index 100% rename from python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/points.py rename to python/spatialdata-js-util/src/spatialdata_js_util/points.py diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/provenance.py b/python/spatialdata-js-util/src/spatialdata_js_util/provenance.py new file mode 100644 index 00000000..40fd5b6a --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/provenance.py @@ -0,0 +1,17 @@ +"""Version and checksum helpers recorded in output manifests.""" + +from __future__ import annotations + +import hashlib +from importlib import metadata + + +def package_version(name: str) -> str | None: + try: + return metadata.version(name) + except metadata.PackageNotFoundError: + return None + + +def sha256(data: bytes | bytearray) -> str: + return hashlib.sha256(data).hexdigest() diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/runners.py b/python/spatialdata-js-util/src/spatialdata_js_util/runners.py similarity index 92% rename from python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/runners.py rename to python/spatialdata-js-util/src/spatialdata_js_util/runners.py index 3f2477ce..0cb29842 100644 --- a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/runners.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/runners.py @@ -16,7 +16,7 @@ write_morton_points_parquet, write_multiscale_points_parquet, ) -from .zarr import ( +from .store import ( copy_points_element_metadata, experimental_points_output_path, list_points_keys, @@ -313,3 +313,36 @@ def run_write_index_permutations( compression=compression, ) ) + + +def run_tables_to_csc( + source: str | Path, + dest: str | Path | None = None, + *, + tables: Sequence[str] | None = None, + layers: bool = True, + densify: bool = False, + overwrite: bool = False, +) -> dict[str, Any]: + from .tables import convert_store_tables_to_csc + + result = _as_command_error( + lambda: convert_store_tables_to_csc( + source, + dest, + tables=tables, + layers=layers, + densify=densify, + overwrite=overwrite, + ) + ) + return result.manifest + + +def run_list_tables(zarr: str | Path) -> dict[str, Any]: + from .store import list_element_keys + + keys = list_element_keys(zarr, "tables") + if not keys: + raise WriterCommandError(f"No table elements found under {Path(zarr) / 'tables'}") + return {"zarr": str(zarr), "table_keys": keys} diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/zarr.py b/python/spatialdata-js-util/src/spatialdata_js_util/store.py similarity index 63% rename from python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/zarr.py rename to python/spatialdata-js-util/src/spatialdata_js_util/store.py index 894e46e0..9bc24b96 100644 --- a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/zarr.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/store.py @@ -1,3 +1,11 @@ +"""Filesystem-level helpers for SpatialData Zarr stores. + +These operate on the store layout directly rather than through `spatialdata`, so +that copy-then-rewrite workflows can touch one element without materialising the +whole object — and so the reader-facing parts of this package stay importable +without the heavy write dependencies. +""" + from __future__ import annotations import json @@ -9,6 +17,73 @@ import pyarrow.dataset as ds +def read_json(path: str | Path) -> dict[str, Any]: + return json.loads(Path(path).read_text()) + + +def json_bytes(value: dict[str, Any]) -> bytes: + return json.dumps(value, indent=2, sort_keys=True).encode("utf-8") + + +def write_json(path: str | Path, value: dict[str, Any]) -> None: + target = Path(path) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(json_bytes(value)) + + +def _collect_consolidated_metadata(store_path: Path) -> dict[str, Any]: + metadata: dict[str, Any] = {} + for meta_path in store_path.rglob("zarr.json"): + if meta_path == store_path / "zarr.json": + continue + rel = meta_path.parent.relative_to(store_path).as_posix() + metadata[rel] = read_json(meta_path) + return metadata + + +def refresh_consolidated_metadata(store_path: str | Path) -> None: + """Rewrite the store root's inline consolidated metadata from disk. + + Required after any structural rewrite: readers trust the consolidated copy, + so a stale one hides newly written arrays or reports old codecs. + """ + root = Path(store_path) + root_path = root / "zarr.json" + root_meta = read_json(root_path) + root_meta["consolidated_metadata"] = { + "kind": "inline", + "must_understand": False, + "metadata": _collect_consolidated_metadata(root), + } + write_json(root_path, root_meta) + + +def list_element_keys(zarr_path: str | Path, collection: str) -> list[str]: + """List element names under a top-level store collection (images, tables, …).""" + root = Path(zarr_path) / collection + if not root.is_dir(): + return [] + return sorted( + child.name + for child in root.iterdir() + if child.is_dir() and ((child / "zarr.json").is_file() or (child / ".zgroup").is_file()) + ) + + +def zarr_format_of(path: str | Path) -> int: + """Return the zarr format (2 or 3) a group on disk was written in. + + AnnData groups inside a store must be rewritten in the format they were read + in; mixing formats within one store breaks readers. + """ + node = Path(path) + if (node / "zarr.json").is_file(): + return int(read_json(node / "zarr.json").get("zarr_format", 3)) + if (node / ".zgroup").is_file(): + return int(read_json(node / ".zgroup").get("zarr_format", 2)) + raise FileNotFoundError(f"No zarr group metadata found at {node}") + + def _points_root(zarr_path: Path) -> Path: return zarr_path / "points" @@ -35,15 +110,11 @@ def list_points_keys(zarr_path: str | Path) -> list[str]: ) -def _read_zarr_json(path: Path) -> dict[str, Any]: - return json.loads(path.read_text()) - - def read_points_element_attrs(zarr_path: str | Path, points_key: str) -> dict[str, Any]: element_json = _points_root(Path(zarr_path)) / points_key / "zarr.json" if not element_json.is_file(): raise FileNotFoundError(f"Points element not found: points/{points_key}") - attrs = _read_zarr_json(element_json).get("attributes", {}) + attrs = read_json(element_json).get("attributes", {}) spatialdata_attrs = attrs.get("spatialdata_attrs", {}) if not isinstance(spatialdata_attrs, dict): spatialdata_attrs = {} @@ -88,7 +159,7 @@ def _points_element_consolidated_entry( if isinstance(entry, dict): return json.loads(json.dumps(entry)) element_json = _points_root(zarr_path) / points_key / "zarr.json" - element_doc = _read_zarr_json(element_json) + element_doc = read_json(element_json) return { "attributes": element_doc.get("attributes", {}), "node_type": element_doc.get("node_type", "group"), @@ -100,7 +171,7 @@ def read_store_consolidated_metadata(zarr_path: str | Path) -> dict[str, Any]: root_json = Path(zarr_path) / "zarr.json" if not root_json.is_file(): raise FileNotFoundError(f"Missing store metadata: {root_json}") - doc = _read_zarr_json(root_json) + doc = read_json(root_json) consolidated = doc.get("consolidated_metadata") if not isinstance(consolidated, dict): raise ValueError(f"Store has no consolidated metadata: {root_json}") @@ -119,7 +190,7 @@ def register_points_elements_in_consolidated_metadata( """Register sibling points elements in the store root consolidated metadata.""" store_path = Path(zarr_path) root_json = store_path / "zarr.json" - doc = _read_zarr_json(root_json) + doc = read_json(root_json) consolidated = doc.get("consolidated_metadata") if not isinstance(consolidated, dict): consolidated = {"kind": "inline", "metadata": {}} diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/tables.py b/python/spatialdata-js-util/src/spatialdata_js_util/tables.py new file mode 100644 index 00000000..fabbc171 --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/tables.py @@ -0,0 +1,260 @@ +"""Convert SpatialData table matrices to CSC. + +Browser clients read expression data one *variable* at a time — colour cells by +one gene, then another. In CSR (AnnData's default on disk) a single gene's values +are scattered across every row's slice, so one gene costs a read of essentially +the whole matrix. In CSC that gene is one contiguous run of `indptr[j]:indptr[j+1]`, +so it is a single ranged read. + +The conversion is lossless and reversible: only the sparse layout changes, not +the values, and `scipy`/`anndata` read either layout transparently. It is +therefore safe to apply to a store that other tools also read. +""" + +from __future__ import annotations + +import shutil +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Any, Sequence + +from .errors import WriterCommandError +from .provenance import package_version +from .store import ( + list_element_keys, + read_json, + refresh_consolidated_metadata, + write_json, + zarr_format_of, +) + +if TYPE_CHECKING: + from anndata import AnnData + +#: Matrix keys converted by default: `X` plus every entry of `layers`. +DEFAULT_MATRIX_KEYS = ("X", "layers") + + +@dataclass(frozen=True) +class CscConversion: + store_path: Path + manifest_path: Path | None + manifest: dict[str, Any] + + +def _sparse_format(matrix: Any) -> str | None: + """Return the scipy sparse format string ('csr', 'csc', …) or None if dense.""" + return getattr(matrix, "format", None) + + +def to_csc(matrix: Any, *, densify: bool = False) -> Any: + """Return *matrix* in CSC layout. + + Already-CSC input is returned unchanged. Dense input is only converted when + *densify* is set, because making a dense matrix sparse can multiply its size + rather than shrink it. + """ + import scipy.sparse as sp + + fmt = _sparse_format(matrix) + if fmt == "csc": + return matrix + if fmt is not None: + return matrix.tocsc() + if densify: + return sp.csc_matrix(matrix) + return matrix + + +def anndata_to_csc( + adata: "AnnData", + *, + layers: bool = True, + densify: bool = False, +) -> dict[str, str]: + """Convert `adata.X` (and optionally `adata.layers`) to CSC in place. + + Returns a per-matrix report of ``{key: "csr->csc" | "already-csc" | …}``. + """ + report: dict[str, str] = {} + + before = _sparse_format(adata.X) + converted = to_csc(adata.X, densify=densify) + if converted is not adata.X: + adata.X = converted + report["X"] = _describe(before, _sparse_format(adata.X)) + + if layers: + for key in list(adata.layers.keys()): + # anndata >=0.12 exposes `X` as a `None`-keyed layer alias; converting + # it again would double-report (and rewrite) the matrix handled above. + if key is None: + continue + layer_before = _sparse_format(adata.layers[key]) + layer_converted = to_csc(adata.layers[key], densify=densify) + if layer_converted is not adata.layers[key]: + adata.layers[key] = layer_converted + report[f"layers/{key}"] = _describe( + layer_before, _sparse_format(adata.layers[key]) + ) + return report + + +def _describe(before: str | None, after: str | None) -> str: + if before is None and after is None: + return "dense (unchanged)" + if before == after: + return f"already-{after}" + return f"{before or 'dense'}->{after}" + + +def _read_table(path: Path) -> "AnnData": + import anndata as ad + + return ad.read_zarr(path) + + +#: Group attributes owned by AnnData's own encoding, which it rewrites itself. +_ANNDATA_OWNED_ATTRS = frozenset({"encoding-type", "encoding-version"}) + + +def _group_attrs_path(path: Path) -> tuple[Path, str | None]: + """Return the metadata file holding a group's attributes, and its nesting key.""" + if (path / "zarr.json").is_file(): + return path / "zarr.json", "attributes" + if (path / ".zattrs").is_file(): + return path / ".zattrs", None + raise FileNotFoundError(f"No zarr group attributes found at {path}") + + +def _read_group_attrs(path: Path) -> dict[str, Any]: + meta_path, key = _group_attrs_path(path) + doc = read_json(meta_path) + return dict(doc.get(key, {})) if key else dict(doc) + + +def _merge_group_attrs(path: Path, attrs: dict[str, Any]) -> None: + meta_path, key = _group_attrs_path(path) + doc = read_json(meta_path) + if key: + merged = {**dict(doc.get(key, {})), **attrs} + doc[key] = merged + else: + doc = {**doc, **attrs} + write_json(meta_path, doc) + + +def _write_table(adata: "AnnData", path: Path, *, zarr_format: int) -> None: + """Rewrite a table group, preserving SpatialData's element attributes. + + `AnnData.write_zarr` writes only AnnData's own group attributes, so a plain + rewrite silently drops the `spatialdata-encoding-type`, `region`, + `region_key`, `instance_key`, and `version` keys that SpatialData puts on the + group — and `read_zarr` then fails on the missing version. We therefore + capture the group's attributes first and merge the non-AnnData ones back. + """ + import anndata as ad + + preserved = { + key: value + for key, value in _read_group_attrs(path).items() + if key not in _ANNDATA_OWNED_ATTRS + } + + # AnnData writes whichever zarr format its global setting names; a store must + # not end up with mixed-format groups, so pin it to the format we read. + previous = ad.settings.zarr_write_format + ad.settings.zarr_write_format = zarr_format + try: + if path.exists(): + shutil.rmtree(path) + adata.write_zarr(path) + finally: + ad.settings.zarr_write_format = previous + + if preserved: + _merge_group_attrs(path, preserved) + + +def convert_store_tables_to_csc( + source: str | Path, + dest: str | Path | None = None, + *, + tables: Sequence[str] | None = None, + layers: bool = True, + densify: bool = False, + overwrite: bool = False, + manifest: bool = True, +) -> CscConversion: + """Rewrite a store's table matrices in CSC layout. + + With *dest*, the store is copied first and the source left untouched; without + it, tables are rewritten in place. Pass *tables* to restrict which table + elements are converted (default: all of them). + """ + source_path = Path(source) + if not (source_path / "zarr.json").is_file(): + raise WriterCommandError(f"Not a zarr store (no zarr.json): {source_path}") + + if dest is None: + store_path = source_path + in_place = True + else: + store_path = Path(dest) + in_place = False + if store_path.exists(): + if not overwrite: + raise WriterCommandError( + f"Destination already exists: {store_path}\nPass overwrite to replace it." + ) + shutil.rmtree(store_path) + shutil.copytree(source_path, store_path) + + available = list_element_keys(store_path, "tables") + if not available: + raise WriterCommandError(f"No table elements found under {store_path / 'tables'}") + + selected = list(tables) if tables else available + unknown = [key for key in selected if key not in available] + if unknown: + raise WriterCommandError( + f"Unknown table element(s): {', '.join(unknown)}. Available: {', '.join(available)}" + ) + + reports: list[dict[str, Any]] = [] + for key in selected: + table_path = store_path / "tables" / key + zarr_format = zarr_format_of(table_path) + adata = _read_table(table_path) + matrices = anndata_to_csc(adata, layers=layers, densify=densify) + _write_table(adata, table_path, zarr_format=zarr_format) + reports.append( + { + "path": f"tables/{key}", + "n_obs": int(adata.n_obs), + "n_vars": int(adata.n_vars), + "zarr_format": zarr_format, + "matrices": matrices, + } + ) + + refresh_consolidated_metadata(store_path) + + manifest_data = { + "format": "spatialdata-csc-tables/v1", + "source": str(source_path), + "output": str(store_path), + "in_place": in_place, + "tables": reports, + "packages": { + "anndata": package_version("anndata"), + "scipy": package_version("scipy"), + "spatialdata": package_version("spatialdata"), + "spatialdata-js-util": package_version("spatialdata-js-util"), + "zarr": package_version("zarr"), + }, + } + manifest_path = store_path.with_suffix(".csc-manifest.json") if manifest else None + if manifest_path is not None: + write_json(manifest_path, manifest_data) + return CscConversion(store_path, manifest_path, manifest_data) diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/tui/__init__.py b/python/spatialdata-js-util/src/spatialdata_js_util/tui/__init__.py similarity index 100% rename from python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/tui/__init__.py rename to python/spatialdata-js-util/src/spatialdata_js_util/tui/__init__.py diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/tui/app.py b/python/spatialdata-js-util/src/spatialdata_js_util/tui/app.py similarity index 100% rename from python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/tui/app.py rename to python/spatialdata-js-util/src/spatialdata_js_util/tui/app.py diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/tui/models.py b/python/spatialdata-js-util/src/spatialdata_js_util/tui/models.py similarity index 100% rename from python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/tui/models.py rename to python/spatialdata-js-util/src/spatialdata_js_util/tui/models.py diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/tui/screens.py b/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py similarity index 99% rename from python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/tui/screens.py rename to python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py index 91088dc2..193d40fe 100644 --- a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/tui/screens.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py @@ -42,7 +42,7 @@ verify_morton_parquet, verify_multiscale_parquet, ) -from ..zarr import list_points_keys, read_points_element_attrs +from ..store import list_points_keys, read_points_element_attrs from .app import WriterApp from .models import CommandId, TaskSpec diff --git a/python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/verify.py b/python/spatialdata-js-util/src/spatialdata_js_util/verify.py similarity index 100% rename from python/spatialdata-experimental-writer/src/spatialdata_experimental_writer/verify.py rename to python/spatialdata-js-util/src/spatialdata_js_util/verify.py diff --git a/python/spatialdata-codec-writer/tests/conftest.py b/python/spatialdata-js-util/tests/conftest.py similarity index 100% rename from python/spatialdata-codec-writer/tests/conftest.py rename to python/spatialdata-js-util/tests/conftest.py diff --git a/python/spatialdata-codec-writer/tests/test_htj2k_encode.py b/python/spatialdata-js-util/tests/test_htj2k_encode.py similarity index 75% rename from python/spatialdata-codec-writer/tests/test_htj2k_encode.py rename to python/spatialdata-js-util/tests/test_htj2k_encode.py index 4dc17876..b20e4de5 100644 --- a/python/spatialdata-codec-writer/tests/test_htj2k_encode.py +++ b/python/spatialdata-js-util/tests/test_htj2k_encode.py @@ -3,11 +3,11 @@ import numpy as np import pytest -from spatialdata_codec_writer.htj2k_encode import ( +from spatialdata_js_util.codecs.htj2k_wasm import ( EncoderPool, - encode_htj2k_plane, + encode_htj2k_wasm, encode_script_path, - htj2k_encode_available, + wasm_encode_available, openjph_vendor_dir, shutdown_encoder_pool, ) @@ -20,8 +20,8 @@ def test_vendored_encode_assets_exist() -> None: @pytest.mark.skipif( - not htj2k_encode_available(), - reason="No HTJ2K encoder is available in this environment.", + not wasm_encode_available(), + reason="The openjph-wasm backend (Node.js) is not available in this environment.", ) def test_encoder_pool_round_trip() -> None: shutdown_encoder_pool() @@ -38,8 +38,8 @@ def test_encoder_pool_round_trip() -> None: @pytest.mark.skipif( - not htj2k_encode_available(), - reason="No HTJ2K encoder is available in this environment.", + not wasm_encode_available(), + reason="The openjph-wasm backend (Node.js) is not available in this environment.", ) def test_multi_component_round_trip() -> None: """A z>1 chunk encodes to one multi-component codestream and round-trips losslessly.""" @@ -63,14 +63,14 @@ def test_multi_component_round_trip() -> None: @pytest.mark.skipif( - not htj2k_encode_available(), - reason="No HTJ2K encoder is available in this environment.", + not wasm_encode_available(), + reason="The openjph-wasm backend (Node.js) is not available in this environment.", ) -def test_encode_htj2k_plane_uses_global_pool() -> None: +def test_encode_htj2k_wasm_uses_global_pool() -> None: shutdown_encoder_pool() try: plane = np.zeros((8, 8), dtype=np.uint16) - encoded = encode_htj2k_plane(plane, reversible=True, quality=0.0) + encoded = encode_htj2k_wasm(plane, reversible=True, quality=0.0) assert len(encoded) > 0 finally: shutdown_encoder_pool() diff --git a/python/spatialdata-codec-writer/tests/test_htj2k_encode_demo.py b/python/spatialdata-js-util/tests/test_htj2k_encode_demo.py similarity index 93% rename from python/spatialdata-codec-writer/tests/test_htj2k_encode_demo.py rename to python/spatialdata-js-util/tests/test_htj2k_encode_demo.py index 1b47dc29..4beea630 100644 --- a/python/spatialdata-codec-writer/tests/test_htj2k_encode_demo.py +++ b/python/spatialdata-js-util/tests/test_htj2k_encode_demo.py @@ -2,13 +2,13 @@ import pytest -from spatialdata_codec_writer import htj2k_encode_available +from spatialdata_js_util import htj2k_available from htj2k_fixtures import HTJ2K_ENCODE_DEMO_STORE, htj2k_encode_demo_image_key, write_htj2k_encode_demo_fixtures @pytest.mark.skipif( - not htj2k_encode_available(), + not htj2k_available(), reason="No HTJ2K encoder is available in this environment.", ) def test_htj2k_encode_demo_fixtures(tmp_path: Path) -> None: diff --git a/python/spatialdata-codec-writer/tests/test_htj2k_quality.py b/python/spatialdata-js-util/tests/test_htj2k_quality.py similarity index 88% rename from python/spatialdata-codec-writer/tests/test_htj2k_quality.py rename to python/spatialdata-js-util/tests/test_htj2k_quality.py index c65e7845..c69f67a6 100644 --- a/python/spatialdata-codec-writer/tests/test_htj2k_quality.py +++ b/python/spatialdata-js-util/tests/test_htj2k_quality.py @@ -5,13 +5,13 @@ import pytest -from spatialdata_codec_writer import htj2k_encode_available +from spatialdata_js_util import htj2k_available from htj2k_fixtures import write_htj2k_quality_sweep_manifest @pytest.mark.skipif( - not htj2k_encode_available(), + not htj2k_available(), reason="OpenJPH WASM HTJ2K encoder is not available in this environment.", ) def test_htj2k_quality_sweep_responds_to_quantization(tmp_path: Path) -> None: @@ -21,7 +21,7 @@ def test_htj2k_quality_sweep_responds_to_quantization(tmp_path: Path) -> None: manifest = json.loads(manifest_path.read_text()) assert manifest["format"] == "spatialdata-htj2k-quality-sweep/v1" - assert manifest["encoder"] == "openjph-wasm" + assert manifest["encoder"] == "openjph" assert manifest["image"]["kind"] == "mandelbrot" by_label = {entry["label"]: entry for entry in manifest["qualities"]} diff --git a/python/spatialdata-experimental-writer/tests/test_integration.py b/python/spatialdata-js-util/tests/test_integration.py similarity index 91% rename from python/spatialdata-experimental-writer/tests/test_integration.py rename to python/spatialdata-js-util/tests/test_integration.py index 1495c6cb..033daba8 100644 --- a/python/spatialdata-experimental-writer/tests/test_integration.py +++ b/python/spatialdata-js-util/tests/test_integration.py @@ -9,11 +9,11 @@ import pyarrow as pa import pyarrow.parquet as pq -from spatialdata_experimental_writer.index_permutations import write_index_permutations -from spatialdata_experimental_writer.errors import WriterCommandError -from spatialdata_experimental_writer.points import MORTON_CODE_2D_COLUMN, write_morton_points_parquet -from spatialdata_experimental_writer.runners import run_morton_points_from_zarr -from spatialdata_experimental_writer.zarr import list_points_keys +from spatialdata_js_util.index_permutations import write_index_permutations +from spatialdata_js_util.errors import WriterCommandError +from spatialdata_js_util.points import MORTON_CODE_2D_COLUMN, write_morton_points_parquet +from spatialdata_js_util.runners import run_morton_points_from_zarr +from spatialdata_js_util.store import list_points_keys def _write_points_element( @@ -85,8 +85,9 @@ def test_morton_points_from_zarr_defaults_to_canonical_path(tmp_path: Path) -> N [ sys.executable, "-m", - "spatialdata_experimental_writer.cli", - "morton-points-from-zarr", + "spatialdata_js_util.cli", + "points", + "morton-from-zarr", str(zarr_root), "--points-key", "transcripts", @@ -110,8 +111,9 @@ def test_cli_expected_error_has_no_traceback(tmp_path: Path) -> None: [ sys.executable, "-m", - "spatialdata_experimental_writer.cli", - "list-points", + "spatialdata_js_util.cli", + "points", + "list", str(missing), ], check=False, @@ -212,7 +214,7 @@ def test_write_index_permutations_writes_manifest(tmp_path: Path) -> None: conditions=tuple( condition for condition in __import__( - "spatialdata_experimental_writer.index_permutations", + "spatialdata_js_util.index_permutations", fromlist=["DEFAULT_CONDITIONS"], ).DEFAULT_CONDITIONS if condition.id in {"canonical", "morton"} diff --git a/python/spatialdata-experimental-writer/tests/test_points.py b/python/spatialdata-js-util/tests/test_points.py similarity index 98% rename from python/spatialdata-experimental-writer/tests/test_points.py rename to python/spatialdata-js-util/tests/test_points.py index f1f40b6a..74db31cd 100644 --- a/python/spatialdata-experimental-writer/tests/test_points.py +++ b/python/spatialdata-js-util/tests/test_points.py @@ -5,7 +5,7 @@ import pandas as pd import pyarrow.parquet as pq -from spatialdata_experimental_writer import ( +from spatialdata_js_util import ( MORTON_CODE_2D_COLUMN, build_spatialdata_multiscale_metadata, morton_sort_points, diff --git a/python/spatialdata-codec-writer/tests/test_recompress.py b/python/spatialdata-js-util/tests/test_recompress.py similarity index 96% rename from python/spatialdata-codec-writer/tests/test_recompress.py rename to python/spatialdata-js-util/tests/test_recompress.py index dca0b999..7c18844b 100644 --- a/python/spatialdata-codec-writer/tests/test_recompress.py +++ b/python/spatialdata-js-util/tests/test_recompress.py @@ -8,17 +8,17 @@ import pytest import zarr -from spatialdata_codec_writer import ( +from spatialdata_js_util import ( CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K, HTJ2K_PRESETS, JP2K_PRESETS, - htj2k_encode_available, + htj2k_available, recompress_spatialdata, resolve_recompression_config, ) -from spatialdata_codec_writer.codecs import decode_htj2k_plane, encode_image_plane -from spatialdata_codec_writer.recompress import _preset_encode_options +from spatialdata_js_util.codecs import decode_htj2k_plane, encode_image_plane +from spatialdata_js_util.images import _preset_encode_options from synthetic_images import mandelbrot_plane @@ -175,7 +175,7 @@ def test_recompress_spatialdata_applies_default_image_to_all_images(tmp_path: Pa @pytest.mark.skipif( - not htj2k_encode_available(), + not htj2k_available(), reason="No HTJ2K encoder is available in this environment.", ) def test_recompress_sibling_applies_to_all_images(tmp_path: Path) -> None: @@ -248,7 +248,7 @@ def test_htj2k_presets_do_not_pass_jp2k_rate_control_levels() -> None: @pytest.mark.skipif( - not htj2k_encode_available(), + not htj2k_available(), reason="No HTJ2K encoder is available in this environment.", ) def test_htj2k_balanced_preset_produces_reasonable_chunk_size() -> None: @@ -382,7 +382,7 @@ def test_recompress_rejects_unknown_image_codec(tmp_path: Path) -> None: @pytest.mark.skipif( - htj2k_encode_available(), + htj2k_available(), reason="HTJ2K encode is available; unavailable-path test not applicable.", ) def test_recompress_rejects_htj2k_when_encode_unavailable(tmp_path: Path) -> None: @@ -406,7 +406,7 @@ def test_recompress_rejects_htj2k_when_encode_unavailable(tmp_path: Path) -> Non @pytest.mark.skipif( - not htj2k_encode_available(), + not htj2k_available(), reason="No HTJ2K encoder is available in this environment.", ) def test_recompress_spatialdata_rewrites_image_with_htj2k(tmp_path: Path) -> None: @@ -440,11 +440,14 @@ def test_recompress_spatialdata_rewrites_image_with_htj2k(tmp_path: Path) -> Non assert first_chunk["source_sha256"] == first_chunk["decoded_sha256"] assert int(decoded[0, 0]) == 0 assert result.manifest["images"][0]["codec"] == CODEC_HTJ2K_OPENJPH - assert result.manifest["images"][0]["encoder"] == "openjph-wasm" + assert result.manifest["images"][0]["encoder"] == "openjph" + # Which OpenJPH build produced the bytes is recorded separately, since + # either the native (imagecodecs) or WASM backend may have been selected. + assert result.manifest["images"][0]["encoder_backend"] in {"imagecodecs", "openjph-wasm"} @pytest.mark.skipif( - not htj2k_encode_available(), + not htj2k_available(), reason="No HTJ2K encoder is available in this environment.", ) def test_recompress_sibling_uses_htj2k_key(tmp_path: Path) -> None: diff --git a/python/spatialdata-codec-writer/tests/test_synthetic_images.py b/python/spatialdata-js-util/tests/test_synthetic_images.py similarity index 100% rename from python/spatialdata-codec-writer/tests/test_synthetic_images.py rename to python/spatialdata-js-util/tests/test_synthetic_images.py diff --git a/python/spatialdata-js-util/tests/test_tables.py b/python/spatialdata-js-util/tests/test_tables.py new file mode 100644 index 00000000..013fec47 --- /dev/null +++ b/python/spatialdata-js-util/tests/test_tables.py @@ -0,0 +1,161 @@ +"""CSC conversion of SpatialData table matrices.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import numpy as np +import pytest + +from spatialdata_js_util.errors import WriterCommandError +from spatialdata_js_util.tables import ( + anndata_to_csc, + convert_store_tables_to_csc, + to_csc, +) + +anndata = pytest.importorskip("anndata") +sparse = pytest.importorskip("scipy.sparse") +sd = pytest.importorskip("spatialdata") + + +def _table(n_obs: int = 30, n_vars: int = 8, *, fmt: str = "csr"): + from spatialdata.models import TableModel + + matrix = sparse.random( + n_obs, n_vars, density=0.4, format=fmt, random_state=0, dtype=np.float32 + ) + adata = anndata.AnnData(X=matrix) + adata.obs["region"] = "img" + adata.obs["region"] = adata.obs["region"].astype("category") + adata.obs["instance_id"] = np.arange(n_obs) + return TableModel.parse( + adata, region="img", region_key="region", instance_key="instance_id" + ) + + +def _store(path: Path, **tables): + store = sd.SpatialData(tables=tables or {"table": _table()}) + store.write(path, overwrite=True) + return path + + +class TestToCsc: + def test_converts_csr(self) -> None: + matrix = sparse.random(10, 4, density=0.5, format="csr", random_state=0) + assert to_csc(matrix).format == "csc" + + def test_returns_csc_unchanged(self) -> None: + matrix = sparse.random(10, 4, density=0.5, format="csc", random_state=0) + assert to_csc(matrix) is matrix + + def test_leaves_dense_alone_by_default(self) -> None: + """Sparsifying a dense matrix can grow it, so it is opt-in.""" + dense = np.arange(12, dtype=np.float32).reshape(3, 4) + assert to_csc(dense) is dense + + def test_densify_converts_dense(self) -> None: + dense = np.arange(12, dtype=np.float32).reshape(3, 4) + assert to_csc(dense, densify=True).format == "csc" + + def test_preserves_values(self) -> None: + matrix = sparse.random(20, 6, density=0.5, format="csr", random_state=1) + assert np.allclose(to_csc(matrix).todense(), matrix.todense()) + + +class TestAnndataToCsc: + def test_converts_x_and_reports(self) -> None: + adata = _table() + report = anndata_to_csc(adata) + assert adata.X.format == "csc" + assert report["X"] == "csr->csc" + + def test_skips_the_none_layer_alias(self) -> None: + """anndata >=0.12 exposes X as a None-keyed layer; it must not be re-reported.""" + adata = _table() + report = anndata_to_csc(adata) + assert not any(key.endswith("/None") for key in report) + + def test_converts_named_layers(self) -> None: + adata = _table() + adata.layers["counts"] = sparse.random( + adata.n_obs, adata.n_vars, density=0.4, format="csr", random_state=2 + ) + report = anndata_to_csc(adata) + assert adata.layers["counts"].format == "csc" + assert report["layers/counts"] == "csr->csc" + + def test_no_layers_flag_leaves_layers_untouched(self) -> None: + adata = _table() + adata.layers["counts"] = sparse.random( + adata.n_obs, adata.n_vars, density=0.4, format="csr", random_state=2 + ) + anndata_to_csc(adata, layers=False) + assert adata.layers["counts"].format == "csr" + + +class TestConvertStoreTablesToCsc: + def test_in_place_conversion_is_readable_by_spatialdata(self, tmp_path: Path) -> None: + store = _store(tmp_path / "s.zarr") + before = sd.read_zarr(store).tables["table"].X.todense() + + result = convert_store_tables_to_csc(store) + + reopened = sd.read_zarr(store).tables["table"] + assert reopened.X.format == "csc" + assert np.allclose(reopened.X.todense(), before) + assert result.manifest["tables"][0]["matrices"]["X"] == "csr->csc" + + def test_preserves_spatialdata_element_attrs(self, tmp_path: Path) -> None: + """AnnData's writer drops these; losing them makes read_zarr fail.""" + store = _store(tmp_path / "s.zarr") + attrs_path = store / "tables" / "table" / "zarr.json" + before = json.loads(attrs_path.read_text())["attributes"] + + convert_store_tables_to_csc(store) + + after = json.loads(attrs_path.read_text())["attributes"] + for key in ("spatialdata-encoding-type", "region", "region_key", "instance_key", "version"): + assert after.get(key) == before.get(key), f"lost table attribute {key!r}" + + def test_copies_to_dest_leaving_source_untouched(self, tmp_path: Path) -> None: + store = _store(tmp_path / "s.zarr") + dest = tmp_path / "csc.zarr" + + convert_store_tables_to_csc(store, dest) + + assert sd.read_zarr(dest).tables["table"].X.format == "csc" + assert sd.read_zarr(store).tables["table"].X.format == "csr" + + def test_writes_a_manifest(self, tmp_path: Path) -> None: + store = _store(tmp_path / "s.zarr") + result = convert_store_tables_to_csc(store) + assert result.manifest_path is not None + assert result.manifest_path.is_file() + assert result.manifest["format"] == "spatialdata-csc-tables/v1" + + def test_rejects_existing_dest_without_overwrite(self, tmp_path: Path) -> None: + store = _store(tmp_path / "s.zarr") + dest = tmp_path / "csc.zarr" + dest.mkdir() + with pytest.raises(WriterCommandError, match="already exists"): + convert_store_tables_to_csc(store, dest) + + def test_rejects_unknown_table_key(self, tmp_path: Path) -> None: + store = _store(tmp_path / "s.zarr") + with pytest.raises(WriterCommandError, match="Unknown table"): + convert_store_tables_to_csc(store, tables=["nope"]) + + def test_rejects_a_non_store_path(self, tmp_path: Path) -> None: + with pytest.raises(WriterCommandError, match="Not a zarr store"): + convert_store_tables_to_csc(tmp_path) + + def test_selects_a_single_table(self, tmp_path: Path) -> None: + store = _store(tmp_path / "s.zarr", first=_table(), second=_table()) + + convert_store_tables_to_csc(store, tables=["first"]) + + reopened = sd.read_zarr(store) + assert reopened.tables["first"].X.format == "csc" + assert reopened.tables["second"].X.format == "csr" diff --git a/python/spatialdata-experimental-writer/tests/test_verify.py b/python/spatialdata-js-util/tests/test_verify.py similarity index 95% rename from python/spatialdata-experimental-writer/tests/test_verify.py rename to python/spatialdata-js-util/tests/test_verify.py index 796e9cba..2e4ba373 100644 --- a/python/spatialdata-experimental-writer/tests/test_verify.py +++ b/python/spatialdata-js-util/tests/test_verify.py @@ -6,14 +6,14 @@ import pyarrow as pa import pyarrow.parquet as pq -from spatialdata_experimental_writer.index_permutations import write_index_permutations -from spatialdata_experimental_writer.points import ( +from spatialdata_js_util.index_permutations import write_index_permutations +from spatialdata_js_util.points import ( MORTON_CODE_2D_COLUMN, build_spatialdata_multiscale_metadata, write_morton_points_parquet, write_multiscale_points_parquet, ) -from spatialdata_experimental_writer.verify import ( +from spatialdata_js_util.verify import ( all_passed, verify_index_permutations_manifest, verify_morton_parquet, @@ -153,7 +153,7 @@ def test_verify_index_permutations_manifest(tmp_path) -> None: conditions=tuple( condition for condition in __import__( - "spatialdata_experimental_writer.index_permutations", + "spatialdata_js_util.index_permutations", fromlist=["DEFAULT_CONDITIONS"], ).DEFAULT_CONDITIONS if condition.id in {"canonical", "morton"} diff --git a/python/spatialdata-codec-writer/tests/test_write_synthetic.py b/python/spatialdata-js-util/tests/test_write_synthetic.py similarity index 89% rename from python/spatialdata-codec-writer/tests/test_write_synthetic.py rename to python/spatialdata-js-util/tests/test_write_synthetic.py index 98ecb16b..b6d1475f 100644 --- a/python/spatialdata-codec-writer/tests/test_write_synthetic.py +++ b/python/spatialdata-js-util/tests/test_write_synthetic.py @@ -5,11 +5,11 @@ import pytest -from spatialdata_codec_writer.htj2k_encode import htj2k_encode_available +from spatialdata_js_util.codecs import htj2k_available @pytest.mark.skipif( - not htj2k_encode_available(), + not htj2k_available(), reason="No HTJ2K encoder is available in this environment.", ) def test_write_synthetic_cli(tmp_path: Path) -> None: diff --git a/python/spatialdata-codec-writer/tests/test_writer.py b/python/spatialdata-js-util/tests/test_writer.py similarity index 93% rename from python/spatialdata-codec-writer/tests/test_writer.py rename to python/spatialdata-js-util/tests/test_writer.py index bc16ec8a..b3e2c5a7 100644 --- a/python/spatialdata-codec-writer/tests/test_writer.py +++ b/python/spatialdata-js-util/tests/test_writer.py @@ -5,8 +5,8 @@ import numpy as np import pytest -from spatialdata_codec_writer import htj2k_encode_available -from spatialdata_codec_writer.codecs import decode_htj2k_plane +from spatialdata_js_util import htj2k_available +from spatialdata_js_util.codecs import decode_htj2k_plane from fixture_writer import ( image_to_tczyx, @@ -24,7 +24,7 @@ @pytest.mark.skipif( - not htj2k_encode_available(), + not htj2k_available(), reason="No HTJ2K encoder is available in this environment.", ) def test_write_htj2k_fixture(tmp_path: Path) -> None: @@ -33,7 +33,7 @@ def test_write_htj2k_fixture(tmp_path: Path) -> None: assert fixture.store_path.exists() assert fixture.manifest_path.exists() assert fixture.manifest["codec"] == "experimental.openjph_htj2k" - assert fixture.manifest["encoder"] == "openjph-wasm" + assert fixture.manifest["encoder"] == "openjph" root_attrs = json.loads((fixture.store_path / "zarr.json").read_text())["attributes"] assert root_attrs["spatialdata_attrs"] == experimental_codec_writer_attrs() @@ -73,7 +73,7 @@ def test_write_jpeg2k_fixture(tmp_path: Path) -> None: @pytest.mark.skipif( - not htj2k_encode_available(), + not htj2k_available(), reason="No HTJ2K encoder is available in this environment.", ) def test_write_mandelbulb_fixture(tmp_path: Path) -> None: @@ -82,7 +82,7 @@ def test_write_mandelbulb_fixture(tmp_path: Path) -> None: assert fixture.store_path.exists() assert fixture.manifest_path.exists() assert fixture.manifest["codec"] == "experimental.openjph_htj2k" - assert fixture.manifest["encoder"] == "openjph-wasm" + assert fixture.manifest["encoder"] == "openjph" assert fixture.manifest["image_path"] == f"images/{MANDELBULB_FIXTURE_IMAGE_KEY}" assert fixture.manifest["shape"] == [2, 1, 8, 128, 128] assert list(fixture.manifest["chunks"]) == list(MANDELBULB_FIXTURE_CHUNKS) diff --git a/python/spatialdata-experimental-writer/tests/test_zarr.py b/python/spatialdata-js-util/tests/test_zarr.py similarity index 98% rename from python/spatialdata-experimental-writer/tests/test_zarr.py rename to python/spatialdata-js-util/tests/test_zarr.py index 42d979ff..1d96c63f 100644 --- a/python/spatialdata-experimental-writer/tests/test_zarr.py +++ b/python/spatialdata-js-util/tests/test_zarr.py @@ -8,7 +8,7 @@ import pyarrow.parquet as pq import pytest -from spatialdata_experimental_writer.zarr import ( +from spatialdata_js_util.store import ( experimental_points_output_path, list_points_keys, points_parquet_path, diff --git a/python/spatialdata-js-util/tests/test_zarr_codec.py b/python/spatialdata-js-util/tests/test_zarr_codec.py new file mode 100644 index 00000000..055a4166 --- /dev/null +++ b/python/spatialdata-js-util/tests/test_zarr_codec.py @@ -0,0 +1,169 @@ +"""The zarr shim: stores written with our codecs must open in plain zarr/spatialdata.""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pytest +import zarr + +from spatialdata_js_util.codecs import ( + CODEC_HTJ2K_OPENJPH, + CODEC_JPEG2K, + backend_report, + htj2k_available, +) +from spatialdata_js_util.codecs.backends import ( + BACKEND_IMAGECODECS, + BACKEND_OPENJPH_WASM, + backend_passes_probe, + probe_fixture, +) +from spatialdata_js_util.codecs.zarr_codec import Htj2kCodec, Jpeg2kCodec + + +def test_codecs_resolve_from_the_zarr_registry() -> None: + """Entry-point registration must work without importing our modules first.""" + from zarr.registry import get_codec_class + + assert get_codec_class(CODEC_HTJ2K_OPENJPH) is Htj2kCodec + assert get_codec_class(CODEC_JPEG2K) is Jpeg2kCodec + + +def test_codec_to_dict_matches_on_disk_metadata() -> None: + assert Htj2kCodec().to_dict() == { + "name": CODEC_HTJ2K_OPENJPH, + "configuration": {}, + } + + +def test_codec_from_dict_tolerates_unknown_configuration() -> None: + # Metadata written by another tool should still load rather than raise. + assert isinstance(Htj2kCodec.from_dict({"configuration": {"unexpected": 1}}), Htj2kCodec) + + +@pytest.mark.skipif(not htj2k_available(), reason="No HTJ2K backend available.") +@pytest.mark.parametrize("codec_name", [CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K]) +def test_zarr_array_round_trips_through_the_codec(tmp_path: Path, codec_name: str) -> None: + """A zarr array declaring our codec round-trips through zarr's own pipeline.""" + y, x = np.mgrid[0:64, 0:48] + data = ((x * 5 + y * 3) % 4096).astype(np.uint16) + + store = tmp_path / f"{codec_name.replace('.', '_')}.zarr" + array = zarr.create_array( + store=str(store), + shape=data.shape, + chunks=(32, 24), + dtype="uint16", + serializer=Htj2kCodec() if codec_name == CODEC_HTJ2K_OPENJPH else Jpeg2kCodec(), + compressors=None, + zarr_format=3, + overwrite=True, + ) + array[:] = data + + reopened = zarr.open_array(str(store), mode="r") + assert np.array_equal(np.asarray(reopened[:]), data) + + +@pytest.mark.skipif(not htj2k_available(), reason="No HTJ2K backend available.") +def test_codec_decodes_multi_component_chunks(tmp_path: Path) -> None: + """Chunks with non-singleton leading axes decode component-wise, not flattened.""" + components, height, width = 3, 16, 12 + data = np.stack( + [(np.arange(height * width, dtype=np.uint16).reshape(height, width) + c * 500) + for c in range(components)] + ) + + store = tmp_path / "volume.zarr" + array = zarr.create_array( + store=str(store), + shape=data.shape, + chunks=data.shape, + dtype="uint16", + serializer=Htj2kCodec(), + compressors=None, + zarr_format=3, + overwrite=True, + ) + array[:] = data + + reopened = np.asarray(zarr.open_array(str(store), mode="r")[:]) + assert reopened.shape == data.shape + # Each plane must survive independently — the failure mode documented in + # docs/multi-component-codec-findings.md was every plane collapsing to plane 0. + assert np.array_equal(reopened, data) + + +class TestBackendProbe: + def test_probe_fixture_is_multi_component(self) -> None: + codestream, expected = probe_fixture() + assert len(codestream) > 0 + assert expected.ndim == 3 + assert expected.shape[0] > 1, "probe must exercise more than one component" + # Distinct planes, so a decoder that replicates plane 0 cannot pass. + assert not np.array_equal(expected[0], expected[1]) + + def test_selected_backend_passes_the_probe(self) -> None: + report = backend_report() + selected = report["selected"] + if selected is None: + pytest.skip("No HTJ2K backend available.") + if selected == BACKEND_IMAGECODECS: + assert report["backends"][BACKEND_IMAGECODECS]["passes_multicomponent_probe"] + + def test_unknown_backend_never_passes(self) -> None: + assert backend_passes_probe("not-a-real-backend") is False + + @pytest.mark.parametrize("name", [BACKEND_IMAGECODECS, BACKEND_OPENJPH_WASM]) + def test_available_backends_decode_our_codestream(self, name: str) -> None: + """Whatever backends exist here must agree with the committed fixture.""" + from spatialdata_js_util.codecs.backends import _BACKENDS + + if not _BACKENDS[name].available(): + pytest.skip(f"{name} backend not available.") + assert backend_passes_probe(name), ( + f"{name} is installed but decodes the multi-component probe incorrectly" + ) + + +class TestSpatialDataReadsOurStores: + """The point of the shim: `spatialdata.read_zarr` opens what we write.""" + + @staticmethod + def _write_source(path: Path) -> np.ndarray: + sd = pytest.importorskip("spatialdata") + from spatialdata.models import Image2DModel + + y, x = np.mgrid[0:128, 0:96] + plane = (((np.sin(x / 7.0) * np.cos(y / 5.0) + 1) * 6000)).astype(np.uint16) + pixels = np.stack([plane, (plane // 3).astype(np.uint16)]) + sd.SpatialData( + images={"morphology": Image2DModel.parse(pixels, dims=("c", "y", "x"))} + ).write(path, overwrite=True) + return pixels + + @pytest.mark.skipif(not htj2k_available(), reason="No HTJ2K backend available.") + @pytest.mark.parametrize("codec_name", [CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K]) + def test_lossless_recompressed_store_reads_back_exactly( + self, tmp_path: Path, codec_name: str + ) -> None: + sd = pytest.importorskip("spatialdata") + from spatialdata_js_util import recompress_spatialdata + + source = tmp_path / "source.zarr" + pixels = self._write_source(source) + + dest = tmp_path / "recompressed.zarr" + recompress_spatialdata( + source, + dest, + codec=codec_name, + preset="lossless", + chunks="auto", + overwrite=True, + ) + + read_back = np.asarray(sd.read_zarr(dest).images["morphology"].data.compute()) + assert np.array_equal(read_back, pixels) diff --git a/python/spatialdata-codec-writer/uv.lock b/python/spatialdata-js-util/uv.lock similarity index 56% rename from python/spatialdata-codec-writer/uv.lock rename to python/spatialdata-js-util/uv.lock index 51b022d1..f3f9e086 100644 --- a/python/spatialdata-codec-writer/uv.lock +++ b/python/spatialdata-js-util/uv.lock @@ -1,10 +1,18 @@ version = 1 revision = 3 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] [[package]] name = "aiobotocore" -version = "3.7.0" +version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -15,23 +23,23 @@ dependencies = [ { name = "python-dateutil" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/75/42cce839c2ec263ff74b10b650fe36b066fbb124cbee6f247eac0983e1ab/aiobotocore-3.7.0.tar.gz", hash = "sha256:c64d871ed5491a6571948dd48eabd185b46c6c23b64e3afd0c059fc7593ada30", size = 127054, upload-time = "2026-05-09T10:02:52.332Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/a7/bc31b7046c610471f0630819ca5d2a57ac4efa8d47135cb53e43f2785390/aiobotocore-3.8.0.tar.gz", hash = "sha256:80a1eb64ea915f3af3c1518669975bae74a17b2f37c14eb0fa2f83b915974670", size = 131368, upload-time = "2026-07-17T03:10:30.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/5f/85535dfb3cfd6442d66d1df1694062c5d6df02f895329e7e120b2a3d2b8b/aiobotocore-3.7.0-py3-none-any.whl", hash = "sha256:680bde7c64679a821a9312641b759d9497f790ba8b2e88c6959e6273ee765b8e", size = 89539, upload-time = "2026-05-09T10:02:50.389Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f4/5a7d76dc844d3ff8ed1f1a043158aa393794aebb787d3e2f8c0fe87f674f/aiobotocore-3.8.0-py3-none-any.whl", hash = "sha256:8bc605132cadfe844a3f334635a0a64fa5e360a4a206e915d99d53db5b6deeba", size = 91169, upload-time = "2026-07-17T03:10:28.771Z" }, ] [[package]] name = "aiohappyeyeballs" -version = "2.6.2" +version = "2.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, ] [[package]] name = "aiohttp" -version = "3.14.1" +version = "3.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -43,90 +51,90 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" }, - { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" }, - { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" }, - { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" }, - { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" }, - { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" }, - { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" }, - { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" }, - { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" }, - { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" }, - { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" }, - { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" }, - { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" }, - { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" }, - { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, - { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, - { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, - { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, - { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, - { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, - { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, - { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, - { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, - { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, - { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, - { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, - { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, - { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, - { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, - { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, - { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, - { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, - { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, - { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, - { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, - { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, - { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, - { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, - { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, - { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, - { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, - { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, - { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, - { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, - { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, - { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, ] [[package]] @@ -153,7 +161,7 @@ wheels = [ [[package]] name = "anndata" -version = "0.12.16" +version = "0.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "array-api-compat" }, @@ -164,17 +172,27 @@ dependencies = [ { name = "packaging" }, { name = "pandas" }, { name = "scipy" }, - { name = "scverse-misc" }, + { name = "scverse-misc", extra = ["settings"] }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, { name = "zarr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/0f/4c6b3a4c5dc57bd02f0656666aefae7e83b8158b839f60cfb1c5f2fbecab/anndata-0.12.16.tar.gz", hash = "sha256:26d557fd147728993aac99c2eef676e1e316a84bbeee3c176ca6c5d66e5fc443", size = 2255740, upload-time = "2026-05-18T15:59:40.346Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/2e/077f2dbd47a727793e0369b81377ceb3077f86e94d61850d3f4c21e28706/anndata-0.13.2.tar.gz", hash = "sha256:f40106369a08204bc89915759113124f12abb19c3d011f7503e627e9ef8f29c9", size = 2283406, upload-time = "2026-07-13T18:08:27.864Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/31/bb8da9d45d2b54b7d7ba31945b7024374028274d089828dfd15035411828/anndata-0.12.16-py3-none-any.whl", hash = "sha256:2661332ed8e757886aafddc2f76103846ae17a16e1c003d76c3001ad067d8bc6", size = 175291, upload-time = "2026-05-18T15:59:38.72Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/75122318c4777678abc163d4db855176828003237267c4326947f8cfcb32/anndata-0.13.2-py3-none-any.whl", hash = "sha256:1930305dc3a32ab5433dee2bc441cd264f9eeed22cc5a224b9f9d69e74d4fdf0", size = 187532, upload-time = "2026-07-13T18:08:26.322Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] [[package]] name = "annsel" -version = "0.1.2" +version = "0.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anndata" }, @@ -185,9 +203,9 @@ dependencies = [ { name = "tqdm" }, { name = "universal-pathlib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/93/cdbe4903acb43fd743832e0c2933b88ec2dfd2a87b147992c45b90f647c8/annsel-0.1.2.tar.gz", hash = "sha256:a9ab14495f3e28c216a5cb74f918542710ac82e22bb04db57af233bbbf536da8", size = 341977, upload-time = "2026-01-06T00:07:19.823Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/59/a7ef7cd8038a62660c7d409abcc97ca2885337f867b61d96667dafcfa16c/annsel-0.1.3.tar.gz", hash = "sha256:6ef0341057f57a93294bec98ab8f4aef83be67ea280389b544ad668b220fbb04", size = 267941, upload-time = "2026-06-22T23:47:49.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/43/fff37380b704b60fad17c298b631044d98e3db8b9721df7897c967facf51/annsel-0.1.2-py3-none-any.whl", hash = "sha256:9659f60f4ee5545bc030341b6b8e932e0a615b95ac83d9cb6e2bd50951ae26da", size = 19120, upload-time = "2026-01-06T00:07:18.69Z" }, + { url = "https://files.pythonhosted.org/packages/69/21/3587d7b32dfade1532d9cbb67291cb26f4353c296cadcda6767bd62458d3/annsel-0.1.3-py3-none-any.whl", hash = "sha256:16eb2d520be086f8e9c8e04cf3e75a5f29814d72f602b48166c10af82bc1eb9d", size = 16760, upload-time = "2026-06-22T23:47:47.74Z" }, ] [[package]] @@ -210,110 +228,98 @@ wheels = [ [[package]] name = "botocore" -version = "1.43.0" +version = "1.43.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/79/2f4be1896db3db7ccf44504253a175d56b6bd6b669619edc5147d1aa21ea/botocore-1.43.0.tar.gz", hash = "sha256:e933b31a2d644253e1d029d7d39e99ba41b87e29300534f189744cc438cdf928", size = 15286817, upload-time = "2026-04-29T22:07:31.723Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/f1/1917891851ac5ac09bb9f4862b8fc9252a009d7c24e8688bb67e4383d9e7/botocore-1.43.46.tar.gz", hash = "sha256:59f2e1ac3cdc66d191cae91c0804bc41847ce817dc8147cf43eaada8f76a5533", size = 15694635, upload-time = "2026-07-10T19:32:00.437Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl", hash = "sha256:cc5b15eaec3c6eac05d8012cb5ef17ebe891beb88a16ca13c374bfaece1241e6", size = 14970102, upload-time = "2026-04-29T22:07:27Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f2/4bd8f2f419088feb3ce55f0ca91040ff902f402edfd197450b20a2e1d533/botocore-1.43.46-py3-none-any.whl", hash = "sha256:cb673891e623ae6e6a1bf24d94ef169504f3eb02584adb5d5bee2f6aae819b60", size = 15380350, upload-time = "2026-07-10T19:31:57.616Z" }, ] [[package]] name = "certifi" -version = "2026.5.20" +version = "2026.7.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[package]] name = "click" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -345,7 +351,7 @@ wheels = [ [[package]] name = "dask" -version = "2026.1.1" +version = "2026.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -356,9 +362,9 @@ dependencies = [ { name = "pyyaml" }, { name = "toolz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/46/61ecde57bac647ca7eb6ffef8dcd90af6c1c649020874cd7fd8738003d62/dask-2026.1.1.tar.gz", hash = "sha256:12b1dbb0d6e92f287feb4076871600b2fba3a843d35ff214776ada5e9e7a1529", size = 10994732, upload-time = "2026-01-16T12:35:30.258Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/39/cbd21c9133d02b4e60899ed466ad5e876553ea68ebffee6209e7dcafc8d4/dask-2026.7.1.tar.gz", hash = "sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7", size = 11548794, upload-time = "2026-07-14T01:06:22.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/4b/9cc373120658a2516aa5f6dcdde631c95d714b876d29ad8f8e009d793f3f/dask-2026.1.1-py3-none-any.whl", hash = "sha256:146b0ef2918eb581e06139183a88801b4a8c52d7c37758a91f8c3b75c54b0e15", size = 1481492, upload-time = "2026-01-16T12:35:22.602Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5f/7c22733da92b3a6cc4dddcaa8731089d213c2790bbc997e51c429a4e8f8b/dask-2026.7.1-py3-none-any.whl", hash = "sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6", size = 1496882, upload-time = "2026-07-14T01:06:20.341Z" }, ] [package.optional-dependencies] @@ -419,7 +425,7 @@ wheels = [ [[package]] name = "distributed" -version = "2026.1.1" +version = "2026.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -435,12 +441,11 @@ dependencies = [ { name = "tblib" }, { name = "toolz" }, { name = "tornado" }, - { name = "urllib3" }, { name = "zict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/19/0c13efdffc55cb311594f66c1c8d36a3c4711e427c820155fb9c59138b5e/distributed-2026.1.1.tar.gz", hash = "sha256:3d2709a43912797df3c345af3bb333bbf1a386ec1e9e6a134e5f050521373dbd", size = 2102870, upload-time = "2026-01-16T12:34:58.258Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/e4/567125e6f2f504a6b1e37e3aecd936cf61b662c09e6e79270bd37c93f703/distributed-2026.7.1.tar.gz", hash = "sha256:f7c48ab6961734874521574d75898466703bce93f22f0cff5872c7e4ac6425d4", size = 2461209, upload-time = "2026-07-14T01:20:55.349Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/dc/6d709bcf8fed7611d8a510aeed23b0436cf6db5b61e63c8eb8451eb0d4d8/distributed-2026.1.1-py3-none-any.whl", hash = "sha256:506759b1ed88e45e12ba65e2a429de9911862db55d27dd8bb293c6268430374e", size = 1008417, upload-time = "2026-01-16T12:34:55.535Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4c/369f0b5665afa8ad6fb1862da979dbf61a650ea2b99b5dccda8953aa327c/distributed-2026.7.1-py3-none-any.whl", hash = "sha256:64eaf4406a04ad26acdc5b8e32fad6bac522d07539225a530bab49610b0114a9", size = 1012001, upload-time = "2026-07-14T01:20:53.684Z" }, ] [[package]] @@ -546,11 +551,11 @@ wheels = [ [[package]] name = "fsspec" -version = "2026.4.0" +version = "2026.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, ] [package.optional-dependencies] @@ -563,7 +568,7 @@ s3 = [ [[package]] name = "geopandas" -version = "1.1.3" +version = "1.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -573,9 +578,9 @@ dependencies = [ { name = "pyproj" }, { name = "shapely" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/ba/8e6b2091878e99e86a36a814dcaeff652ed48bdb03d53e78e15aaa63a914/geopandas-1.1.3.tar.gz", hash = "sha256:91a31989b6f566012838d21d5f8033f37dce882079ccb7cfdc40d5ccce7f284f", size = 336718, upload-time = "2026-03-09T21:49:09.545Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/a9/0478b74a33aea66827c10baaf6025cb2a17e44b1c117533eecd3e977bb75/geopandas-1.1.4.tar.gz", hash = "sha256:06f2890a07e1a239047daa14b486a7c6ae5ce82dcf7405e13c46bf31f5d0dd66", size = 337445, upload-time = "2026-06-26T17:23:54.497Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/78/6a04792ace63a93e162f1305392d500ae8ddcb620e7eb88a22fd622b35bb/geopandas-1.1.3-py3-none-any.whl", hash = "sha256:90d62a64f95eaa3be2ccc115c5f3d6e24208bb11983b390fdc0621a3eccd0230", size = 342514, upload-time = "2026-03-09T21:49:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/59/a8/bd530cc264e62ddbc1d1bb7225823992e6f2432c664693e9281bb6b9c359/geopandas-1.1.4-py3-none-any.whl", hash = "sha256:1a0c459cbdb1537cd154dafe6174be20d1760844b7f1c967dc8520b180f2e773", size = 343292, upload-time = "2026-06-26T17:23:53.112Z" }, ] [[package]] @@ -655,43 +660,43 @@ wheels = [ [[package]] name = "imagecodecs" -version = "2026.6.6" +version = "2026.6.26" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/d5/d653e9c636b606fae7f5d3813b5c9ed2df5f184547b7d5842cf64540bfb3/imagecodecs-2026.6.6.tar.gz", hash = "sha256:c7b014f5c0e73ff455700b1db271415d3c35a3bddedca6527cb2eefcd2fa8a10", size = 9710513, upload-time = "2026-06-06T15:02:10.48Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/9f/b436418349779e1c18cc27575360cad03dd492bc574ae9e297832bc48cab/imagecodecs-2026.6.26.tar.gz", hash = "sha256:da95b145f6b4f746acc9e0b8707b164eefc6a36ade3b6e70f74d102e9affad8c", size = 9669990, upload-time = "2026-06-28T18:27:01.957Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/e0/0b17f51e1c9ee8d02270ed7a3d3c124eafeb0df3b4f007e8c1b4b1268676/imagecodecs-2026.6.6-cp312-abi3-macosx_10_15_x86_64.whl", hash = "sha256:f2d35d6771251d128556fe144b9f8492a7189c4525d5fd0d7c8318d6fcbfb390", size = 14943911, upload-time = "2026-06-06T15:01:17.687Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2e/e10694fc6ba70d9188cc1ac86ce60a3c57208721280f011f0f9ac4e1c225/imagecodecs-2026.6.6-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:c3052aed81b3d348c77163b475ab00252964a5f7caf74ae9c62d4ca90e247fe8", size = 12540505, upload-time = "2026-06-06T15:01:20.628Z" }, - { url = "https://files.pythonhosted.org/packages/5a/23/70e01368c354448dd96b5781a77aeb4fde5eee50ecb9fcc2c5d41fbe25cb/imagecodecs-2026.6.6-cp312-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:907bb9b858c3dc1d5c4ff91385ea210ae90fa2fb00b44206643a24e20db742d2", size = 27148793, upload-time = "2026-06-06T18:12:15.003Z" }, - { url = "https://files.pythonhosted.org/packages/30/65/66720d8f2745792b5653ae48327b57e99c54f8548fcff05f1a25ebd86d6c/imagecodecs-2026.6.6-cp312-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:bca94547db076322c606f37d501f6e7ab3fa3f9eee3fcd6f233854c67014e9fb", size = 28132122, upload-time = "2026-06-06T15:01:24.558Z" }, - { url = "https://files.pythonhosted.org/packages/36/d3/2dba651d8d830b2823a3e750fe231cf183f1eb11f88d4ca280450efeab57/imagecodecs-2026.6.6-cp312-abi3-win32.whl", hash = "sha256:00596506fcea638063491db1baeef9a2445b3e0147bcbd76633561df702b1eb1", size = 16480894, upload-time = "2026-06-06T15:01:28.639Z" }, - { url = "https://files.pythonhosted.org/packages/20/2a/62a84ce74794664c62d571266b1c8c13261e9dbe82dfc1f0fd1887b6c68b/imagecodecs-2026.6.6-cp312-abi3-win_amd64.whl", hash = "sha256:3ba2bcee53dabfb7d4dbd6f3c3ae731f3bfa43fa9c936d969886dff2f50e2160", size = 20220064, upload-time = "2026-06-06T15:01:32.128Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/90daf65a4e9b6136fc36b59a0d23aac020707ae8f831de36eceba2a7241f/imagecodecs-2026.6.6-cp312-abi3-win_arm64.whl", hash = "sha256:031c5ea4d0c8bfacc87a5ca0ffdfee9a06b64265960d8b0b5d3f4a32434404c3", size = 16041147, upload-time = "2026-06-06T15:01:35.371Z" }, - { url = "https://files.pythonhosted.org/packages/62/0d/d953120557150f31e879c6cdb977a89a1d987eb063048d9be83fbc62d1ce/imagecodecs-2026.6.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5ee77053841dc7a8fbe59df774c0456fbe604edf2cd12c795c29deee1384ce8d", size = 15339668, upload-time = "2026-06-06T15:01:39.935Z" }, - { url = "https://files.pythonhosted.org/packages/73/1b/cec19d0428188f85b78981087e837e9f30166f013cdf98227a1b20e2c38d/imagecodecs-2026.6.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1fe0a7c8fb67bdd1bbb406c0e830dd1652acf6dce62620071a47e20f7d00619a", size = 12987803, upload-time = "2026-06-06T15:01:42.829Z" }, - { url = "https://files.pythonhosted.org/packages/e6/73/ad06b8b5944b063ad5f6f55253bb44b64673fd1a776567f7895ffc25d788/imagecodecs-2026.6.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2673fc78fc6e2178b13957eccaa2e97c716ea03607af6a2a44a2f09d00919f3f", size = 32256108, upload-time = "2026-06-06T18:12:19.557Z" }, - { url = "https://files.pythonhosted.org/packages/9e/32/2e67ec87bbcd1c4055f97c3962f5f49598e9c63d2809711c88084959c7dd/imagecodecs-2026.6.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ed0760b9d428408798c010593ef204f88d949a9f57fa037623544e811c462c67", size = 32630019, upload-time = "2026-06-06T15:01:47.239Z" }, - { url = "https://files.pythonhosted.org/packages/6f/13/4ebe5100ff4daf4acb2220481cb7a0d18e623eec7458b85c846b6ec850f0/imagecodecs-2026.6.6-cp314-cp314t-win32.whl", hash = "sha256:16540ed96854312d235bb86f885cb5f6ca2f4f1acde8e283d13a2bda2fb01e26", size = 17234217, upload-time = "2026-06-06T15:01:50.672Z" }, - { url = "https://files.pythonhosted.org/packages/d7/3a/4298fa62a5d94ce860e0d12082c96707f8b952b499f669daf9b3031716fc/imagecodecs-2026.6.6-cp314-cp314t-win_amd64.whl", hash = "sha256:325f72db99e2678c9ad0ccca8ac9f274b6ac7b4a1a5df71db0f96e23f964aeab", size = 21259530, upload-time = "2026-06-06T15:01:54.316Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fb/b5eb0049b31558e439af0e06f3b206ac0b45930773d15abcb0236c0399a1/imagecodecs-2026.6.6-cp314-cp314t-win_arm64.whl", hash = "sha256:fcd8b838bbe10c6b312b8aa67add4bb375bd1537ae4d92815c1ea522bef43cbc", size = 16759012, upload-time = "2026-06-06T15:01:57.729Z" }, - { url = "https://files.pythonhosted.org/packages/1a/84/46857efdc71a8ddb6632858534a74625025824ca77285655a49c246a25bd/imagecodecs-2026.6.6-cp315-cp315t-win32.whl", hash = "sha256:b20be458a4d4a0283acaae17c94ad8128dfc6ce62fefdd55f1882c8e3a67850c", size = 17230579, upload-time = "2026-06-06T15:02:00.722Z" }, - { url = "https://files.pythonhosted.org/packages/71/cc/91dca22a7bfd0905da6e82c2f6777087294ad07a8e87fdc8727bc6def326/imagecodecs-2026.6.6-cp315-cp315t-win_amd64.whl", hash = "sha256:51703586bb553d3b3ef9a0ab5c5b26432611c2bbb262f5f8fdeb19095c5e130f", size = 21253502, upload-time = "2026-06-06T15:02:04.283Z" }, - { url = "https://files.pythonhosted.org/packages/3d/95/cf1c29b96ba3d2b644e27eac92389876e286d28391b046b4f486cee6fde5/imagecodecs-2026.6.6-cp315-cp315t-win_arm64.whl", hash = "sha256:e4a059c2f57df75f99024bf9499b17b10d5a3916c5b5addba31317a523f120cd", size = 16757440, upload-time = "2026-06-06T15:02:07.924Z" }, + { url = "https://files.pythonhosted.org/packages/7e/03/89bf1a370dca9860b13414f63d7106600c527305d9d8c0369c15a93ca794/imagecodecs-2026.6.26-cp312-abi3-macosx_10_15_x86_64.whl", hash = "sha256:468314982db54c9c6c32567b159de5d97cf5273763cdede53e320414787ad80a", size = 15387254, upload-time = "2026-06-28T18:26:00.528Z" }, + { url = "https://files.pythonhosted.org/packages/05/47/826d50cd4378a6982d8a750300b9adc81c3458c4c1bdfd979affc8ce5492/imagecodecs-2026.6.26-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:2d3298028a74d748e5b7a00bd736d41cdf2372861376e4af916818e853ca5fc6", size = 12696122, upload-time = "2026-06-28T18:26:03.714Z" }, + { url = "https://files.pythonhosted.org/packages/c6/52/ccd8a8a71bb2d0ed9fd4a2592028ab129f6b41c6f2b0ef6c1b9251ec7c88/imagecodecs-2026.6.26-cp312-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73b4310cab61edf5c74ec5cb19672963c2ceb42239877256ca30eb1b70849cf3", size = 27215507, upload-time = "2026-06-28T18:26:07.479Z" }, + { url = "https://files.pythonhosted.org/packages/d8/3a/29fa169992185df4f1e98b3a1d2b70aac5903170c33f6d66cae68f488c01/imagecodecs-2026.6.26-cp312-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6e9e57171ce7cdf884e7d45d25427f77275321a0ddbd26941c5c90b242d2b597", size = 28855566, upload-time = "2026-06-28T18:26:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/27d82b105d871ceb6de3a68afbee5f95551b8fba796e4e6e35e334f994f4/imagecodecs-2026.6.26-cp312-abi3-win32.whl", hash = "sha256:091b66cfe11e37e5a037ff64b262bede3898f779ebd9cbf0d4a108613577ab15", size = 16568720, upload-time = "2026-06-28T18:26:15.259Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/8d27dbe855293380194aa794cbcce1df2d23039058b3d917817aaa3f5547/imagecodecs-2026.6.26-cp312-abi3-win_amd64.whl", hash = "sha256:0758a273acd197e236e67f1597d630924f62c12f5b74837dc7eef2db72c1a888", size = 21275600, upload-time = "2026-06-28T18:26:19.678Z" }, + { url = "https://files.pythonhosted.org/packages/ab/38/3d0d0a4abc3ba607f23a3237a1cb6f7737dbd1b39498364f01f62c004e8b/imagecodecs-2026.6.26-cp312-abi3-win_arm64.whl", hash = "sha256:8bbf132cce699c6b282106e97edc65946d788ccca79f6ba8f5cd1b8a3f43423c", size = 16849832, upload-time = "2026-06-28T18:26:22.797Z" }, + { url = "https://files.pythonhosted.org/packages/f4/30/040baf3050ee9803608ff9694a4a4dc9ec868e222fe4c5c8de481388ffb0/imagecodecs-2026.6.26-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:46980597f48fe790f81e286cf662f0323d0022203cbe48f77c3be608f510de85", size = 15803759, upload-time = "2026-06-28T18:26:25.873Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e8/042eeac7c78565f1b4c14ba3dc6a96aad9b958e008a1e291a1cbf72123e9/imagecodecs-2026.6.26-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c48767bbc6ed0b40df3607cafdf59dac6f4467b91c26e835744d7ee4085cfdf7", size = 13164944, upload-time = "2026-06-28T18:26:29.33Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/0e0c86df9a55a0a90ced8cf5214abb459015cda5f2e713e0495ccd9bf23f/imagecodecs-2026.6.26-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9c130cf207efcb35acf74d6040317d217294640e478aa733fb51be758bcaddbc", size = 32436234, upload-time = "2026-06-28T18:26:33.35Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9a/2d63eb2ffa46863bf0ea05884ce1ea2a73a5153b428937b8cbe4faa81536/imagecodecs-2026.6.26-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:014f5f809418eb3dfe29badd4af29ddeeaba8b5eabda4bd502e2292727e4094f", size = 33446845, upload-time = "2026-06-28T18:26:38.238Z" }, + { url = "https://files.pythonhosted.org/packages/14/68/ece973cd5b1d8305b95ad827b3c6033aed166c5ce93780b6092dbfd5deec/imagecodecs-2026.6.26-cp314-cp314t-win32.whl", hash = "sha256:daa276e5645750404a28a911dcbfb94c7d32b2d8903b5f05167474ea02bd5b40", size = 17330378, upload-time = "2026-06-28T18:26:42.172Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/ab2e9ee84e44768ceb73bb9301988ea4ab9022f02ef9d824d6ebb641a0fb/imagecodecs-2026.6.26-cp314-cp314t-win_amd64.whl", hash = "sha256:0718d75672768871aa6adac2129c8f74c167fb4a3da25f660f3afc94b420304f", size = 22122461, upload-time = "2026-06-28T18:26:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/58/53/7943321eb84c6d39e71ca47e689557a976c6ebafec48b759b3be0bd7c601/imagecodecs-2026.6.26-cp314-cp314t-win_arm64.whl", hash = "sha256:69b90b7b729d33cd9417d3d572d3092d2d49f7974638ee00e2cdc3ed1fba11d6", size = 17657147, upload-time = "2026-06-28T18:26:48.924Z" }, + { url = "https://files.pythonhosted.org/packages/f8/de/130820754c21c00dfdc6852661de2bb30161b5a439a8643a446dae97a02f/imagecodecs-2026.6.26-cp315-cp315t-win32.whl", hash = "sha256:a10c347225f5fb1b9d1a36cfadccad9b173609c30e0dd3f68706675d6f4f9b58", size = 17330338, upload-time = "2026-06-28T18:26:52.04Z" }, + { url = "https://files.pythonhosted.org/packages/a7/df/b4f6f2a9e9102ca674673cb1b432bbebfe2d99acb6364390768024d56ae6/imagecodecs-2026.6.26-cp315-cp315t-win_amd64.whl", hash = "sha256:85ab027a8bc900f13b1cdaac05bb602ea58749731ae18743a7956642a7da7a2c", size = 22116857, upload-time = "2026-06-28T18:26:55.65Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c4/d87b33e8df5c6ec19f5cd1d5ab39195b2077ac21536ff5146c20d3fb8694/imagecodecs-2026.6.26-cp315-cp315t-win_arm64.whl", hash = "sha256:41e18fcd2124c083b00f988eeb9c5cf89274e5c2f473ee03d54761b71048085c", size = 17651071, upload-time = "2026-06-28T18:26:59.305Z" }, ] [[package]] name = "imageio" -version = "2.37.3" +version = "2.37.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "pillow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/62/aa770a9307508d2a2a2c62d536a49347bffe9e55322db27838d3c93d0b07/imageio-2.37.4.tar.gz", hash = "sha256:e45cbc5e83502047fb138f7f585f7f105a136a57eea5f4b3cfc6ce1b52720bd3", size = 390173, upload-time = "2026-07-20T05:26:11.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2d/ca050652104bab2cf55e569db2a178b1b61cb041fef28307f2db383f6d9f/imageio-2.37.4-py3-none-any.whl", hash = "sha256:1ab2e22c8debf700f24c3ac43e8f95f3b3a8110c83b93411e97b4b0b2cd1c7e6", size = 318000, upload-time = "2026-07-20T05:26:09.874Z" }, ] [[package]] @@ -745,28 +750,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/5b/058db09c45ba58a7321bdf2294cae651b37d6fec68117265af90cde043b0/legacy_api_wrap-1.5-py3-none-any.whl", hash = "sha256:5a8ea50e3e3bcbcdec3447b77034fd0d32cb2cf4089db799238708e4d7e0098d", size = 10182, upload-time = "2025-11-03T13:21:11.102Z" }, ] +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + [[package]] name = "llvmlite" -version = "0.47.0" +version = "0.48.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" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/a0/acc8ffcd5bdc63df0097e22c719bfcd61b604358343089313a8aebbb24ab/llvmlite-0.48.0.tar.gz", hash = "sha256:543b19f9ef8f3c7c60d1468191e4ee1b1537bf9f8a3d56f64c0ddd98de92edd2", size = 184016, upload-time = "2026-07-02T20:20:05.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/48/4b7fe0e34c169fa2f12532916133e0b219d2823b540733651b34fdac509a/llvmlite-0.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f", size = 37232769, upload-time = "2026-03-31T18:28:43.735Z" }, - { 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/2f/f5/d281ae0f79378a5a91f308ea9fdb9f9cc068fddd09629edc0725a5a8fde1/llvmlite-0.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7", size = 38138692, upload-time = "2026-03-31T18:28:57.147Z" }, - { url = "https://files.pythonhosted.org/packages/77/6f/4615353e016799f80fa52ccb270a843c413b22361fadda2589b2922fb9b0/llvmlite-0.47.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a3c6a735d4e1041808434f9d440faa3d78d9b4af2ee64d05a66f351883b6ceec", size = 37232771, upload-time = "2026-03-31T18:29:01.324Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/d6/da/b32cafcb926fb0ce2aa25553bf32cb8764af31438f40e2481df08884c947/llvmlite-0.47.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c6951e2b29930227963e53ee152441f0e14be92e9d4231852102d986c761e40", size = 55128632, upload-time = "2026-03-31T18:29:11.235Z" }, - { url = "https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e", size = 38138695, upload-time = "2026-03-31T18:29:15.43Z" }, - { url = "https://files.pythonhosted.org/packages/1c/d4/33c8af00f0bf6f552d74f3a054f648af2c5bc6bece97972f3bfadce4f5ec/llvmlite-0.47.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:de966c626c35c9dff5ae7bf12db25637738d0df83fc370cf793bc94d43d92d14", size = 37232773, upload-time = "2026-03-31T18:29:19.453Z" }, - { url = "https://files.pythonhosted.org/packages/64/1d/a760e993e0c0ba6db38d46b9f48f6c7dceb8ac838824997fb9e25f97bc04/llvmlite-0.47.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddbccff2aeaff8670368340a158abefc032fe9b3ccf7d9c496639263d00151aa", size = 56275176, upload-time = "2026-03-31T18:29:24.149Z" }, - { url = "https://files.pythonhosted.org/packages/84/3b/e679bc3b29127182a7f4aa2d2e9e5bea42adb93fb840484147d59c236299/llvmlite-0.47.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a7b778a2e144fc64468fb9bf509ac1226c9813a00b4d7afea5d988c4e22fca", size = 55128631, upload-time = "2026-03-31T18:29:29.536Z" }, - { url = "https://files.pythonhosted.org/packages/be/f7/19e2a09c62809c9e63bbd14ce71fb92c6ff7b7b3045741bb00c781efc3c9/llvmlite-0.47.0-cp314-cp314-win_amd64.whl", hash = "sha256:694e3c2cdc472ed2bd8bd4555ca002eec4310961dd58ef791d508f57b5cc4c94", size = 39153826, upload-time = "2026-03-31T18:29:33.681Z" }, - { url = "https://files.pythonhosted.org/packages/40/a1/581a8c707b5e80efdbbe1dd94527404d33fe50bceb71f39d5a7e11bd57b7/llvmlite-0.47.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:92ec8a169a20b473c1c54d4695e371bde36489fc1efa3688e11e99beba0abf9c", size = 37232772, upload-time = "2026-03-31T18:29:37.952Z" }, - { url = "https://files.pythonhosted.org/packages/11/03/16090dd6f74ba2b8b922276047f15962fbeea0a75d5601607edb301ba945/llvmlite-0.47.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa1cbd800edd3b20bc141521f7fd45a6185a5b84109aa6855134e81397ffe72b", size = 56275178, upload-time = "2026-03-31T18:29:42.58Z" }, - { url = "https://files.pythonhosted.org/packages/f5/cb/0abf1dd4c5286a95ffe0c1d8c67aec06b515894a0dd2ac97f5e27b82ab0b/llvmlite-0.47.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6725179b89f03b17dabe236ff3422cb8291b4c1bf40af152826dfd34e350ae8", size = 55128632, upload-time = "2026-03-31T18:29:46.939Z" }, - { url = "https://files.pythonhosted.org/packages/4f/79/d3bbab197e86e0ff4f9c07122895b66a3e0d024247fcff7f12c473cb36d9/llvmlite-0.47.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6842cf6f707ec4be3d985a385ad03f72b2d724439e118fcbe99b2929964f0453", size = 39153839, upload-time = "2026-03-31T18:29:51.004Z" }, + { url = "https://files.pythonhosted.org/packages/92/a2/28696a9e61e245d1a79816d29d106692a90a2b6e7d78c98b326db70827af/llvmlite-0.48.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:d66c3beb4209087ddd4cf4ed2a0856b6887e6a913bdcf1aacfec9851cf2cba4e", size = 40480651, upload-time = "2026-07-01T18:41:35.694Z" }, + { url = "https://files.pythonhosted.org/packages/80/f2/72409351db66d0a317ec5087e076f31fb7b773a640db8a90ce6b5cac9edd/llvmlite-0.48.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:416fa4c2c66c2c6dc6d0a402648c19206e548efa0aa1eff01ad5cdad0af8217d", size = 59890118, upload-time = "2026-07-01T18:41:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/3a/27/5ae2f3722606360480707adb47f001ad89df8251d06b14ee80336e660b66/llvmlite-0.48.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5e5a5131045b72345c71062ea1a91910dde913792b6c9b28ebb2c1c0a712e98", size = 58343459, upload-time = "2026-07-01T18:41:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/78/d824ffff7521cd140dc2006e44ce2bc82e64b48d1b32e90e956308c85a74/llvmlite-0.48.0-cp312-cp312-win_amd64.whl", hash = "sha256:d45c7541a80934ec6d8ab0defe67439494ecd2193cbf852a44ba827808976ac1", size = 41865022, upload-time = "2026-07-01T18:41:48.663Z" }, + { url = "https://files.pythonhosted.org/packages/9c/23/fe9316d14626b42c73ef0b502e724705a6ee9450afe53759c0a99c37c2d7/llvmlite-0.48.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a83a99ef0c05b4ccddf9b6218ed9fe84b653a0caf7c1d9dbe148d6d16c67f518", size = 40480652, upload-time = "2026-07-01T18:41:52.216Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4a/90715fa12006d681270b08d881195b6fab3ec39572e048764a1f7f59fed7/llvmlite-0.48.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8761b9e522f55207e24424fcd98370289eec2710bf8e915c82d1053f642450dc", size = 59890120, upload-time = "2026-07-01T18:42:00.748Z" }, + { url = "https://files.pythonhosted.org/packages/70/5e/7b3e20d64650ca3c80af0cdb664ec4b575ec83d9d4dd05bea8bd31f9bbb6/llvmlite-0.48.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fe5cb59b2063bfa039dcb8ca6481c0181bf552f340d10dcf61d7996a665556e", size = 58343457, upload-time = "2026-07-01T18:41:56.41Z" }, + { url = "https://files.pythonhosted.org/packages/17/97/5a430055d1838cf1fb7a01cfa943300f5e4c026fc6333a522c5e4a03b0c1/llvmlite-0.48.0-cp313-cp313-win_amd64.whl", hash = "sha256:91c7e24e74cde3f02b88aa5acca678373f9e069f3b98531b3dbb3a142d9d10bb", size = 41865022, upload-time = "2026-07-01T18:42:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8e/8170f2e0c217f88069c333d85bb976e536b332aecfcce606ddbdb249385f/llvmlite-0.48.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:321f1ac39b462603f0b589751aecf2d237d056f6d005749c1752b6f23ec3f074", size = 40480650, upload-time = "2026-07-01T18:42:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e1/05b50692b647cac3c18200ac485b04f342f00ed173c9cc46767274469a15/llvmlite-0.48.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:05f0103c8f2f96a37441337e3643863c01b8e83e530aff38960dcb383c54a065", size = 59890115, upload-time = "2026-07-01T18:42:17.805Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c3/470b8c4ff9ae2db2f9cf5c3e73de76ed908a32788ae9eb5602d43e6a476b/llvmlite-0.48.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37d66fae72802175b0bfe1ea06e624b51e2d7aee6c3c34bbd09739b8f88e8e0b", size = 58343457, upload-time = "2026-07-01T18:42:13.217Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2d/6a5171fb7236ac0895e1a02ccba3735bf291e8597239aa6421894d3c0ba8/llvmlite-0.48.0-cp314-cp314-win_amd64.whl", hash = "sha256:966dcab0a598e2bd8fb5f2cc082cf7b07bae564fc485a3a8692393caf986facf", size = 42986372, upload-time = "2026-07-01T18:42:21.483Z" }, + { url = "https://files.pythonhosted.org/packages/94/e3/7a93e09c9f94e637ca90209ceef0334a9a1d45b0bdb7c92ff922d25d6187/llvmlite-0.48.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a5c413317050a1d67c34708bde97707f9b2257ef1017f7532d21fe7d9a9ff30", size = 40480654, upload-time = "2026-07-01T18:42:25.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/98/a29133b4728671a175f7d616fab8b1c6e1d8c269d1523581d3160697bfb1/llvmlite-0.48.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d9f952dff6c350c529997423d4fa43abae9722a884ac7ffafc37a3af676e7db", size = 59890119, upload-time = "2026-07-01T18:42:33.88Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cf/7aac11a1f1c7ec54b60c7f6814e87561fb6b55b2f290455d7941eb113420/llvmlite-0.48.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:054aa7d46595935565f276cf0c1659b4f10929c996dd4a606875fae26fba2a23", size = 58343460, upload-time = "2026-07-01T18:42:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/db/41/b96f440c7df5ebba07872cad4e30fbc3560387755b1ea0b629adb76d5ca8/llvmlite-0.48.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d0b3c61aac83b42fb48cc96bffbf57c81b82b2aa92276b7ed6420c814629a99a", size = 42986383, upload-time = "2026-07-01T18:42:37.544Z" }, ] [[package]] @@ -790,6 +807,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -853,6 +875,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -873,54 +907,54 @@ wheels = [ [[package]] name = "msgpack" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/23/6139781ca7aadf656fa8e384fa84693ffb13f299e6931b6526427fe5e297/msgpack-1.2.0.tar.gz", hash = "sha256:8e17af38197bf58e7e819041678f6178f4491493f5b8c8580414f40f7c2c3c41", size = 183017, upload-time = "2026-06-11T04:16:10.775Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/07/dcb13f37e670257c8d0e944f116c799c34ac6968ecb48c83619f7e91d8b5/msgpack-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2d6047ccd11a12c96a69f2bfe026471abef67334c3d0494a93e5310e45140a2", size = 82888, upload-time = "2026-06-11T04:15:08.992Z" }, - { url = "https://files.pythonhosted.org/packages/84/5f/6643b2a6a36ca4bc73c7674831be1d4d581cceecc7eb019dba1915951739/msgpack-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0347e3ac0dfee99086d3b68fe959da3f5f657c0019ddbaeaaa259a85f8603422", size = 82223, upload-time = "2026-06-11T04:15:10.182Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c8/9e1668b9897358e5ab39a18142e38be3cf15807e643757782da9f4a53cb3/msgpack-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25552ff1f2ff3dc8333e27eabb94f702da5929ed0e07969688194a3e9f12e151", size = 409700, upload-time = "2026-06-11T04:15:11.441Z" }, - { url = "https://files.pythonhosted.org/packages/38/ed/b7728573156d70b6b094233b0f38d876fc37340826cf852347ec2c7ca8ca/msgpack-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0d94420d9d52c56568159a69200af7e45eadb29615fa9d09fada140de1c38c7", size = 420090, upload-time = "2026-06-11T04:15:12.868Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f7/5ea755a89868c04f9cdf6d96d2d99da4b3d198af10e76a6082dd0fceccc0/msgpack-1.2.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d16e1f2db4a9eebc07b7cc91898d71e710f2eed8358711a605fee802caff8923", size = 378538, upload-time = "2026-06-11T04:15:14.511Z" }, - { url = "https://files.pythonhosted.org/packages/80/2d/126e59332a439c94ffd682c38ca0102b23480e2784b3dac48d8959b0bbac/msgpack-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9cb2e700e85f1e27bbb5c9de6cc1c9a4bc5ac64d5404bdcbcb37a0dc7a947a3", size = 399468, upload-time = "2026-06-11T04:15:16.133Z" }, - { url = "https://files.pythonhosted.org/packages/da/f9/7abcef683a0ad2e5ab3a4940344aad9f20cdf1f42057ecb0982cf55085d6/msgpack-1.2.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:717d0b166dd176a5f786aeafff081f6439680acf5af193eb63e6266c12b04d3d", size = 374212, upload-time = "2026-06-11T04:15:17.536Z" }, - { url = "https://files.pythonhosted.org/packages/27/23/2d62cf0e971678e96f8a3cfa9bd77fb719ddb98da73790f63c53fd847ad8/msgpack-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e87c7a21654d18111eb1a89bd5c42baba42e61887365d9e89585e112b4203f9e", size = 414361, upload-time = "2026-06-11T04:15:18.99Z" }, - { url = "https://files.pythonhosted.org/packages/32/fb/f5c153f614037aaf802d291a4653ba1bb731f56feacba886f7c21c109e56/msgpack-1.2.0-cp312-cp312-win32.whl", hash = "sha256:967e0c891f5f23ab65762f2e5dc95922759c79f1ef99ef4c7e1fdd863e0d0af9", size = 64389, upload-time = "2026-06-11T04:15:20.237Z" }, - { url = "https://files.pythonhosted.org/packages/90/af/8aafce6e5544b43b84cb670aca40c8bea7eb5ae8f42bfcbdc7098739987a/msgpack-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:6c23e33cee28dcffa112ae205661da4636fd7b06bd9ad1559a890623b92d060b", size = 71185, upload-time = "2026-06-11T04:15:21.51Z" }, - { url = "https://files.pythonhosted.org/packages/ba/08/9cc94be1fc1fe3d1379d439326259aef0344274f64623a8138feb54dff68/msgpack-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:6eeb771571f63f68045433b1a35c0256b946f31ed62f006997e40b8ad8b735af", size = 64481, upload-time = "2026-06-11T04:15:22.639Z" }, - { url = "https://files.pythonhosted.org/packages/7d/26/2902c6946ab5c8fe1e46e40842dfc32b8824464ad5cd4725364fd83f7a58/msgpack-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a1d30df1f302f2b7a7404afbac2ab76d510036c34cf34dffb01f704a7288e45", size = 82621, upload-time = "2026-06-11T04:15:23.844Z" }, - { url = "https://files.pythonhosted.org/packages/c9/59/7e6b812629d2f919e586041bffc130e1af32079f71bb20699eed54ed6d92/msgpack-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:581e317112260d8ca488d490cad9290a5682276f309c41c7de237a85ed8799c8", size = 81866, upload-time = "2026-06-11T04:15:25.032Z" }, - { url = "https://files.pythonhosted.org/packages/31/13/8c291196e60aafdbae38f482205d79432297749ac5d412fe638154fb6f1d/msgpack-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6827d12eacc16873eba62408a1b7bbe8ecfb4a8f7ed78a631ae9bae6ad43cf2", size = 405618, upload-time = "2026-06-11T04:15:26.235Z" }, - { url = "https://files.pythonhosted.org/packages/fb/63/68f5d0ea81e167db5f59ddb94dc6f837667062113feff1c73fabf8907061/msgpack-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a186027e4279efa4c8bf06ce30605498d7d0d3af0fba0b9799dce85a3fd4a93c", size = 416468, upload-time = "2026-06-11T04:15:27.732Z" }, - { url = "https://files.pythonhosted.org/packages/73/58/567dddf5c5a2790f673bcd7d80c83466d68e5ee9a9674ebca3db8101c0c8/msgpack-1.2.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a96142c14a11cf1a509e8b9aaf72858a3b742b7613e095ce646913e88ce7bd99", size = 374464, upload-time = "2026-06-11T04:15:29.286Z" }, - { url = "https://files.pythonhosted.org/packages/0d/30/0c2342fc9092e4498045f5f60bca6ccbe4f4d87789778c2300e6fd6efe82/msgpack-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50c220579b68a6085b95408b2eaa486b259520f55d8e363ddc9b5d7ba5a6ac6d", size = 395879, upload-time = "2026-06-11T04:15:30.973Z" }, - { url = "https://files.pythonhosted.org/packages/b9/11/9565b29b58ce3c33e177b490478b7aaeb8f726ecaaeda26d815893c1db5a/msgpack-1.2.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4dcb9d12ab100ecacdfaaf37a3d72fe8392eacc7054afc1916b12d1b747c8446", size = 371749, upload-time = "2026-06-11T04:15:32.418Z" }, - { url = "https://files.pythonhosted.org/packages/f2/da/7bade19d60b73e2ef73fb76aaf4504c112a70cb760951b7202a0c64b5111/msgpack-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a804727188ab0ebb237fadb303b743f04925a69d8c3247292d1e33e679767c15", size = 410416, upload-time = "2026-06-11T04:15:34.053Z" }, - { url = "https://files.pythonhosted.org/packages/6d/14/c0c619571c02432208a5977a8dbdd3fc65fe1369f8226ca4b6d08cca87d8/msgpack-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1a1ac6ae1fe23298f79380e7b144c8a454e5d05616b0096584f353ba2d750114", size = 64357, upload-time = "2026-06-11T04:15:35.535Z" }, - { url = "https://files.pythonhosted.org/packages/50/a5/de06718460909aa965737fec4cfe8a15dedc6544a8c55feeb6956fa0d6e3/msgpack-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c3c80949d79578f9dc85fd9fb91edfe6694e8a729cd5744634d59d8455fdde3", size = 71057, upload-time = "2026-06-11T04:15:36.83Z" }, - { url = "https://files.pythonhosted.org/packages/c7/52/73446b0141c94a856e22b787c56709c0815fc34f185326577e15b26d8cfe/msgpack-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fcf8f76fa587c2395fd0057c7232dbf071241f9ad280b235adb7ab585289989e", size = 64490, upload-time = "2026-06-11T04:15:38.001Z" }, - { url = "https://files.pythonhosted.org/packages/35/3d/a7e3cdafa8c0cf36c81e2fa848ec4d30cf089459af45b390ad03f9ce6f49/msgpack-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f854fa1a8b55d75d82ef9a905d9cdbeffdf7897c088f6020bd221867da5e56a5", size = 83032, upload-time = "2026-06-11T04:15:39.38Z" }, - { url = "https://files.pythonhosted.org/packages/ca/aa/53ddfba0e347cc4b484e95f629c5850b9e800ca8390c91ffc604407acf87/msgpack-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e90df581f80f53b372d5d9d9349078d729851a3a0d0bd74f53ccb598d01e45b8", size = 82600, upload-time = "2026-06-11T04:15:40.609Z" }, - { url = "https://files.pythonhosted.org/packages/59/fd/e64c2c776e6dbad0af3c963fe0c0dd1ee1ba09efac478b233ab1db41868f/msgpack-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b276ed50d8ac75d1f134a433ae79af8557d0fa25ee5b4737da533dfc2ce382e8", size = 404342, upload-time = "2026-06-11T04:15:41.87Z" }, - { url = "https://files.pythonhosted.org/packages/1b/60/fb9a08e6ccba882dfd370a5837fe3a07572938fdfe954f0f17fdf3e574b9/msgpack-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:544d972459c92aa32e63b800d07c2d9cf2734a3be29cee3a0b478a622850e9f5", size = 412351, upload-time = "2026-06-11T04:15:43.253Z" }, - { url = "https://files.pythonhosted.org/packages/37/4d/df5c575c274fedc68ac9c6c61d045161899efad2afcdc25138efa7edde69/msgpack-1.2.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a070147cc2cf6b8a891734e0f5c8fe8f70ed8739ab30ba140b058005a6e86af4", size = 373331, upload-time = "2026-06-11T04:15:44.754Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a4/c8b98f8191e985ed2003d87664ce3c95cca41db5d0cf6bf4f54327d32ec8/msgpack-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7685e23b0f51745a751629c31713fbefdef8896b31b2bb38299dfa4ae6c0740c", size = 394654, upload-time = "2026-06-11T04:15:46.423Z" }, - { url = "https://files.pythonhosted.org/packages/d4/49/76f036720a602ea24428cfec5ec806f2487c0380b1bff0a2aa3094e15f87/msgpack-1.2.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b9204daeee8d91a7ae5acf2d2a8e3983be9a3025f38aa21bfaefbd7eea84a7dc", size = 370624, upload-time = "2026-06-11T04:15:48.062Z" }, - { url = "https://files.pythonhosted.org/packages/9f/38/40af3d29232833705a43b0fce0d07425cc280a7b92ab2b29932425b40df4/msgpack-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bfc057248609742ebbabf6bcd27fea4fd99c4980584e613c168c9b002318298f", size = 408038, upload-time = "2026-06-11T04:15:49.669Z" }, - { url = "https://files.pythonhosted.org/packages/30/b2/f140ca450524dff4d8d0eb81eb9ed75f8f3e0b1f12e49c5b01617cfa0b1c/msgpack-1.2.0-cp314-cp314-win32.whl", hash = "sha256:a3faa7edf2388337ae849239878e92f0298b4dab4488e4f1834062f9d0c410c9", size = 65823, upload-time = "2026-06-11T04:15:51.062Z" }, - { url = "https://files.pythonhosted.org/packages/4d/13/6517bf966b841c7675ded30701a068ce141f3e698a27aaa35c702d8e078b/msgpack-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:1a3effc392a57744e4681e55d05f97d5ee7b598747d718340a9b4b8a970c40e1", size = 72484, upload-time = "2026-06-11T04:15:52.289Z" }, - { url = "https://files.pythonhosted.org/packages/45/8c/1d948420fdaa24de4efdb8012a6a5bebe09c82ee002b8c2ca745e9917f1f/msgpack-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:56a318f7df6bec7b40928d6b0519961f20a510d8baabf6baa393a70444588f0a", size = 66657, upload-time = "2026-06-11T04:15:53.583Z" }, - { url = "https://files.pythonhosted.org/packages/39/16/1674faa1b7bddc19e79b465fd8e88e2cf4e3f7cae90723740701e8541068/msgpack-1.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:afa4a65ab2097795e771a74a3a81ea49534aaeba874eaf426a3332268e045ae6", size = 86093, upload-time = "2026-06-11T04:15:54.98Z" }, - { url = "https://files.pythonhosted.org/packages/dd/24/f241bcfdd9e96b2246289357c5a5e5a496189fd41c5844bee802c116aac7/msgpack-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:409550770632bb28daa70a11d0ed5763f7db38f40b06f7db9f11dd2794d01102", size = 86372, upload-time = "2026-06-11T04:15:56.381Z" }, - { url = "https://files.pythonhosted.org/packages/94/c9/57f8ab98a1b21808c27b6dd6029053e0a796ffbb9b371e460dbe997011a9/msgpack-1.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf47e3cd11ce044965a9736a322afdd390b31ed602d1c1b10211d1a841f1d587", size = 428207, upload-time = "2026-06-11T04:15:57.739Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/4fd4aa739f131ded751ca7167c8ee87d2aab32506ebbeea893b60b51d343/msgpack-1.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:204bc9f5d6e59c1718c0a4a84fc8ff71b5b4562faac257c1a68bca611ecf9b72", size = 426082, upload-time = "2026-06-11T04:15:59.356Z" }, - { url = "https://files.pythonhosted.org/packages/f9/00/db88e9a08fcd6513decaad06cbd5c168142bc3e662fb2f1aca3a563b7aa1/msgpack-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:610154307b27267266368bc1d1c7bb8aeb71da7be9356d403cb2442d9e6399f5", size = 378355, upload-time = "2026-06-11T04:16:00.916Z" }, - { url = "https://files.pythonhosted.org/packages/54/84/eee4dd703d7a600cf46159d621c070b0b9468cf3dbade4ea8272bf5232a4/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6799f157bb63e79f11e2e590cfdb28423fc18dd60c270c3914b5b4586ae36f7e", size = 410848, upload-time = "2026-06-11T04:16:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/0a/195e2c549fd4631eb7f157d016ff15a10c4c1cf82b6d0a9b1edaef5174b1/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:72bd844902cf0a5ac3af2ef742f253cd0b1e5bcd184f49b4fb9a6a1f7bf305e8", size = 376152, upload-time = "2026-06-11T04:16:04.041Z" }, - { url = "https://files.pythonhosted.org/packages/45/9b/bdd143fa79baec411dc658f5686fed680a18b36fcea5fccb6af1b8c7d832/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3c0bd450f78d0d81722c80da6cdbf674a856967870a9db2f6c4debc4d8b3c67c", size = 417061, upload-time = "2026-06-11T04:16:05.63Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ce/011ffcd8b919f55196ec53f12ae162e21c879d95afba226894314ff62c07/msgpack-1.2.0-cp314-cp314t-win32.whl", hash = "sha256:378caf74c4c718dfc17590ce68a6d710ed398ff6fcf08237de23b77755730b55", size = 70782, upload-time = "2026-06-11T04:16:07.105Z" }, - { url = "https://files.pythonhosted.org/packages/57/a8/9b8791ca96b1be6b9f659c718271e2cb7f99f73f58aad2dd0b30f750f6c0/msgpack-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:553b42598165c4dd3235994fd6e4b0dfb1ce5f3fd33d94ba9609442643015f38", size = 77899, upload-time = "2026-06-11T04:16:08.353Z" }, - { url = "https://files.pythonhosted.org/packages/5b/04/3fa2dffb87bf598696b86bde7cd642d0a7590520c3fa24cd19611dfebeb7/msgpack-1.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2825bb1da548d214ab8a810906b7dd69a10f3838b615a2cc46e5172d3cb44f6e", size = 71004, upload-time = "2026-06-11T04:16:09.556Z" }, +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, ] [[package]] @@ -1051,11 +1085,11 @@ wheels = [ [[package]] name = "narwhals" -version = "2.22.1" +version = "2.24.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/3c/c4ef2164a71c1a63d7f1ae411c4082c5fa872405106db60a4b7114989ad7/narwhals-2.22.1.tar.gz", hash = "sha256:d62920805a0a43b7ff8b54b0c0d3142d796f8a9301836ada37e573d6a33cbcd9", size = 647493, upload-time = "2026-06-05T12:34:34.051Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/ca/36339329c4604adbcc99c899b7eb1ce1a555c499b6a6860757dc9bfed36d/narwhals-2.22.1-py3-none-any.whl", hash = "sha256:60567d774edf77db53906f89d9fbd164e66e56d66d388e1e6990f17ac33cfb53", size = 454815, upload-time = "2026-06-05T12:34:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, ] [package.optional-dependencies] @@ -1083,30 +1117,30 @@ wheels = [ [[package]] name = "numba" -version = "0.65.1" +version = "0.66.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llvmlite" }, { name = "numpy" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/a0/570e3dc53e5602b49108f62a13e529f1eec8bfc7ef37d49c825924dcf546/numba-0.66.0.tar.gz", hash = "sha256:b900e63a0e26c05ea9a6d5a3a5a0a177cb64c5011887bf43edb8c3ed2c38d363", size = 2806181, upload-time = "2026-07-01T23:12:46.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/bc/76f8f8c5cf9adee47fdb7bbb03be8900f76f902d451d7477cf12b845e1de/numba-0.65.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ac3f1e77c352dd0ea9712732c2d8f9ca507717435eec5b5013bf138ac33c4a08", size = 2681371, upload-time = "2026-04-24T02:02:26.105Z" }, - { 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/db/9e/3c679b2ee078425b9e99a91e44f8d132a6830d8ccce5227bc5e9181aeed8/numba-0.65.1-cp312-cp312-win_amd64.whl", hash = "sha256:5971c632be2a2351500431f46213821dba8d02b18a9f7d02fd36bd2743e41a6a", size = 2750611, upload-time = "2026-04-24T02:02:31.477Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/14a4579049c1eb673afd0de0cb4842982acd55b9ce2643e763db858bcea0/numba-0.65.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1735c15c1134a5108b4d6a5c77fc0947924ea066a738dc09a52008c13df9cad3", size = 2681344, upload-time = "2026-04-24T02:02:33.65Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/62/08/e16a8b5d9a018962ebb5c66be662317cde32b9f5dab08441f90bed5522fb/numba-0.65.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:594a8680b3fadac99e97e489b1fd89007177e5336713745c3b769528c635a464", size = 3509783, upload-time = "2026-04-24T02:02:38.245Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a5/03c970d57f4c1741354837353ce39fb5206952ae1dba8922d29c86f64805/numba-0.65.1-cp313-cp313-win_amd64.whl", hash = "sha256:85be74c0d036842699a30058f82fb88fc5ffdc59f7615cab5792ea92914c9b62", size = 2750534, upload-time = "2026-04-24T02:02:39.903Z" }, - { url = "https://files.pythonhosted.org/packages/4f/2e/8aed9b726d9ba5f11ad287645fd479e88278db3060a25cb1225d730eb2b7/numba-0.65.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:33f5eb68eb1c843511615d14663ce60258525d6a4c65ab040e2c2b0c4cf17450", size = 2681554, upload-time = "2026-04-24T02:02:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/87/96/f3eb235fafa82a34e2ab5dd7dc9ffff998ebf5f0bbc23fa56a96aeb44da6/numba-0.65.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71e73029bf53a62cc6afcf96be4bd942290d8b4c55f0a454fb536158115790f7", size = 3779602, upload-time = "2026-04-24T02:02:43.726Z" }, - { url = "https://files.pythonhosted.org/packages/09/90/b0f09b48752d23640b8284f22aa597737e8adaddc7fbfacc4708b7f73a4c/numba-0.65.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a07635e0be926b9bdbffb09137c230fb13f6ec0e564914ba937cee12ce3eb35", size = 3479532, upload-time = "2026-04-24T02:02:45.427Z" }, - { url = "https://files.pythonhosted.org/packages/56/46/3f7fc04fb853559e74b210e0b62c19974ec844cefec611f9e535f4da3761/numba-0.65.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a20fcdabdefbdacf88d85caf70c3b18c4bcb7ebb8f82e6a19486383dd26ab63", size = 2752637, upload-time = "2026-04-24T02:02:47.664Z" }, - { url = "https://files.pythonhosted.org/packages/81/7b/c1a341a9067367778f4152a5f01061cf281fb09582c92c510ec4918cabf6/numba-0.65.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:548dd4b3a4508d5062768d1514b2cd7b015f9a25ec7af651c50dee243965e652", size = 2684600, upload-time = "2026-04-24T02:02:49.653Z" }, - { url = "https://files.pythonhosted.org/packages/03/36/98ddbcf3e4f04a6dd07e1c67249955920579ba4af6bb6868e3088f4ed282/numba-0.65.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:78abc28feff2c2ff8307fff3975b6438352759c9acb797ecd6b1fb6e7e39e31d", size = 3817198, upload-time = "2026-04-24T02:02:51.266Z" }, - { url = "https://files.pythonhosted.org/packages/a3/83/0dad21057ece5a835599f5d24099b091703995e23dbbf894f259e91c010b/numba-0.65.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7676cb389555805f9b9a1840cbcd1ea6c8bd5376ab6918e3a29c5ea1dbda20", size = 3533862, upload-time = "2026-04-24T02:02:52.987Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/8be7118ffd4c8440881046eac3d0982cc5ab42909508cf5d67024d62a2e4/numba-0.65.1-cp314-cp314t-win_amd64.whl", hash = "sha256:20609346e3bd75204950dcbbfe383a8d7dbf4902f442aedbf00f97fef4aa8f38", size = 2758237, upload-time = "2026-04-24T02:02:54.612Z" }, + { url = "https://files.pythonhosted.org/packages/62/a3/70deb7f88461c1cd5d16aa990c2380604102661a427667b8950dcdccc27f/numba-0.66.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:53ca5900b7cab15109796030113a6b28576bae5ad7bb507ad6dd1360ddd81ba4", size = 2727264, upload-time = "2026-07-01T23:12:18.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/25c319845e9a4e08f16611ddbda56a192eb7b6ed13e1a2bff2da272ffb97/numba-0.66.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0999e3ee1b18c48e1fb51d11af35ef59852c7f4f50569c9550c25faef0616ad1", size = 3866252, upload-time = "2026-07-01T23:12:20.429Z" }, + { url = "https://files.pythonhosted.org/packages/71/ef/a82d6fd6bf1b0fe461651e924d3647eeec9ac17f8eee4896264bf7480930/numba-0.66.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efe0d2d5099790df945e0cb6e1b3104bd965d7bbfac50d62f1d5d1d6ade0825d", size = 3566974, upload-time = "2026-07-01T23:12:22.116Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/9e6171e378822ab191c7abcfd3d8cfc8644516f6c7834c22e210e4acc070/numba-0.66.0-cp312-cp312-win_amd64.whl", hash = "sha256:b075a4e7ebc43dc6294f223e2821659656209fd5e0ce53245877c23d66d6e1a9", size = 2797403, upload-time = "2026-07-01T23:12:23.724Z" }, + { url = "https://files.pythonhosted.org/packages/03/52/176c02d005c5c5143cde10a85bbcdcb6236d9e34c3aac089380e0506cd1d/numba-0.66.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:380b2556a2019ccd1e956ae77dd257eaa39403f7520768b626d44b755112785e", size = 2727084, upload-time = "2026-07-01T23:12:25.434Z" }, + { url = "https://files.pythonhosted.org/packages/44/b5/e930010965568fe7f2c6c962fd2849d458cb9f62c3ab7584af8a19a2b40a/numba-0.66.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:939316d5d8619751207b8972a67852b5a7646665298cb4de693cd6bf135152f4", size = 3873663, upload-time = "2026-07-01T23:12:27.308Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ec/5b51457cbe96e4831141d83e892e65191b23a1b78728456c62909d231ace/numba-0.66.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdf506775d9f02eb92a87bf5c5b1e0d25506fd18cafd769f4ed914a8feac73e7", size = 3573529, upload-time = "2026-07-01T23:12:28.944Z" }, + { url = "https://files.pythonhosted.org/packages/83/7e/cea7710e96913d3c7f2999f16db1b28e6c5be5171cbf40f77f98333a7243/numba-0.66.0-cp313-cp313-win_amd64.whl", hash = "sha256:c5bfe5350284509ab0474390321454c3a8627a188af5b68c910e83df3e2db4a7", size = 2797247, upload-time = "2026-07-01T23:12:30.774Z" }, + { url = "https://files.pythonhosted.org/packages/96/7a/7e0e73550eb4e41ede6e72fb5371f4539537a4d770a3b73fa9b61aea0622/numba-0.66.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:46ae5f2b19e2af3c33c2df100306a90ea2f981c8158b0390f8bf6c20eee7357e", size = 2727296, upload-time = "2026-07-01T23:12:32.39Z" }, + { url = "https://files.pythonhosted.org/packages/0f/26/885774c006de6620ed3d10f45d8e20fe0b8e6aad6d573211a2cbc8b3e528/numba-0.66.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e2b101f23b8b63978d334574d2039f27f0dccfe1d891756f33a2e2f3e4c88cf4", size = 3842720, upload-time = "2026-07-01T23:12:33.938Z" }, + { url = "https://files.pythonhosted.org/packages/93/99/edebf7de890b73973d839dd971cf73734adfb81ffa1b4504f84b9059c3e5/numba-0.66.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63b943eb2c9ba371908ce2cd6dfc643db51fc40f7966993376a1701bc922f537", size = 3543537, upload-time = "2026-07-01T23:12:35.566Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/b46ad28ac3681d035ea21365c5e052149062e1a0a9affd0563d2760ea6ff/numba-0.66.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd57790acd20f6a468e0ad333ef6b82355e309a92310fb7dff80e919f01a21a9", size = 2799250, upload-time = "2026-07-01T23:12:37.154Z" }, + { url = "https://files.pythonhosted.org/packages/10/6f/5e77a7397a37dd16f57a7b72e7e470db5227b68e3639df0d13a8e674883d/numba-0.66.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:db7735d15ea17a283d6485b9fa3504769f78fd86e5146638ad5e8da57c031b9e", size = 2730342, upload-time = "2026-07-01T23:12:38.758Z" }, + { url = "https://files.pythonhosted.org/packages/39/fd/e9c9680a3813f3d781c20e5d53c1074801b787d4feecca0472fdd7c05ce1/numba-0.66.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:651a2b53298340956db26ecbe7ab043106b50a40c2807e66d617a4917245a4ab", size = 3878695, upload-time = "2026-07-01T23:12:40.302Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/9b363287b85fcd4537ea3878793822878b2ac1008a78159d2096fea628de/numba-0.66.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c1144ba1720ea59ad79f4f488ed54d149b2613b357f7e445678b7d0739c70e9", size = 3596323, upload-time = "2026-07-01T23:12:42.805Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f2/dca53d50b8f2289dd01954ace9da261e0487d5b74b188b4304e4ecc3492c/numba-0.66.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d426178fb991a85714c43112a8ea7b9d9579ea856ad8dcdb9c1c3941903ba5be", size = 2804772, upload-time = "2026-07-01T23:12:44.399Z" }, ] [[package]] @@ -1199,7 +1233,7 @@ wheels = [ [[package]] name = "ome-zarr" -version = "0.15.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1207,15 +1241,30 @@ dependencies = [ { name = "deprecated" }, { name = "fsspec", extra = ["s3"] }, { name = "numpy" }, + { name = "ome-zarr-models" }, { name = "rangehttpserver" }, { name = "requests" }, { name = "scikit-image" }, { name = "toolz" }, { name = "zarr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/ee/0e99c62dd3d453c9fb84d8c8cc9f64e255c2d7638546836516282fae2be5/ome_zarr-0.15.0.tar.gz", hash = "sha256:e9479d3a9849c194a616355290f6ae574a7d4402559c5f7005be20cf81945435", size = 81497, upload-time = "2026-04-08T11:57:25.475Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/98/f39cf0ec00ad64996ed2d2d6059d15a30c7b8f559cd2a216589885001a6a/ome_zarr-0.18.0.tar.gz", hash = "sha256:5cc0fc682b0c177fa32e54f4479b2436f79850a171e83ad4c6b08fec32e66cd0", size = 106039, upload-time = "2026-06-17T16:22:07.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/be/2cf9389c9a7b80ceefae88bd713afa6a4b488cb47e524e7dadae44260efd/ome_zarr-0.18.0-py3-none-any.whl", hash = "sha256:aaf6ea6ac5790623290210efc566ad49f92da170b50b07e9113b6b06a0870cc5", size = 57947, upload-time = "2026-06-17T16:22:06.732Z" }, +] + +[[package]] +name = "ome-zarr-models" +version = "1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "pydantic-zarr" }, + { name = "zarr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/17/f4aa0b46a59ba4e7875dc031f5401f709c9fe987bed9932b067c64bac0e2/ome_zarr_models-1.7.tar.gz", hash = "sha256:3979d304f7669e7a22be0bd8a6dd5dcc93fe85af3d842e56930d2ef2e6929090", size = 107192, upload-time = "2026-04-16T12:23:19.147Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/71/159de9929fe98326ada41393d2133fda3f9869ea10bf94996c48846ff68f/ome_zarr-0.15.0-py3-none-any.whl", hash = "sha256:99789b0c4c27fbd197ab1a8bb3ae58b1c7b4e72d0d6b95e9dda6e6416623d3eb", size = 45200, upload-time = "2026-04-08T11:57:24.399Z" }, + { url = "https://files.pythonhosted.org/packages/80/dd/3e3beee6716a4da1b60dc7dfc5d06d697bd015b11ed70c6c0bc0368aabc6/ome_zarr_models-1.7-py3-none-any.whl", hash = "sha256:4e786af38936e62be50cee000c53bf41091ee7d24b0d304570b43147ef34b6bb", size = 64185, upload-time = "2026-04-16T12:23:17.769Z" }, ] [[package]] @@ -1229,49 +1278,48 @@ wheels = [ [[package]] name = "pandas" -version = "2.3.3" +version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, - { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, - { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, - { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, - { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, - { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, - { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, - { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, - { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, - { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, - { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, - { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, - { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, - { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, - { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, - { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, - { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, - { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, - { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, - { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, - { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, - { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, - { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, - { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, - { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, ] [[package]] @@ -1307,71 +1355,73 @@ wheels = [ [[package]] name = "pillow" -version = "12.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +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/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, ] [[package]] @@ -1389,11 +1439,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/b8/02/5bf3639f5b77e9b18 [[package]] name = "platformdirs" -version = "4.10.0" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, ] [[package]] @@ -1543,45 +1593,38 @@ wheels = [ [[package]] name = "pyarrow" -version = "24.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, - { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, - { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, - { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, - { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, - { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, - { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, - { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, - { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, - { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, - { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, - { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, - { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, - { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, - { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, - { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, - { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, - { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/f3/95428098d1fa7d04432fb750eed06b41304c2f6a5d3319985e64db2d9d41/pyarrow-25.0.0.tar.gz", hash = "sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4", size = 1199181, upload-time = "2026-07-10T08:29:50.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/44/fdd3a4377807b7dcabe2d4b5aa99dbbc98e2e5df3f1ca4e7f0aec492d987/pyarrow-25.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e", size = 35850884, upload-time = "2026-07-10T08:26:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/bf/71/9f053177a7709b8c90abb00a2375b916286f9f0d6cfb21a5cadd4ef811e8/pyarrow-25.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec", size = 37616197, upload-time = "2026-07-10T08:26:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/95/1a/22bfb6597dcdc861fa83c39c06e1457cb56f698940eff42fbb25de30e8e5/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f", size = 46841966, upload-time = "2026-07-10T08:27:07.685Z" }, + { url = "https://files.pythonhosted.org/packages/55/0e/cd705c042bc4fe7022478db577fcab4abdcfabb9bc37ab7a75556b3fcb2b/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778", size = 50088993, upload-time = "2026-07-10T08:27:14.268Z" }, + { url = "https://files.pythonhosted.org/packages/98/ee/d822e1ee31fe31ec5d057210e0605c950b975dcd8d9a332976cc859a9df8/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537", size = 49941005, upload-time = "2026-07-10T08:27:21.274Z" }, + { url = "https://files.pythonhosted.org/packages/33/1b/207a90cc64619a095eb75a263ae069735f2810056d43c667befd573ec083/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b", size = 53112355, upload-time = "2026-07-10T08:27:27.911Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fe/81d1e5f8beed15c01e98649d5c6e2167b67fd395884a2488f18bf1cf0dba/pyarrow-25.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5", size = 27945954, upload-time = "2026-07-10T08:27:32.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c8/098ce17d778fd9d29e40bb8c5f19a40cc90c3f0b46c9057b0d7993f42f54/pyarrow-25.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc", size = 35844549, upload-time = "2026-07-10T08:27:37.956Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/24c28877219abf6263d909b1592c97ff82c59f13a59acbed11fc87c0654f/pyarrow-25.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e", size = 37610397, upload-time = "2026-07-10T08:27:43.803Z" }, + { url = "https://files.pythonhosted.org/packages/53/55/6d1d5f5aff317ec5de9421594679ed51ed828fe7e2ce209327f819d801e4/pyarrow-25.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be", size = 46841701, upload-time = "2026-07-10T08:27:49.741Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5d/f790fb6965ab54c9da0dda7856abc75fd0d7648d865f8d603c111d203a64/pyarrow-25.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9", size = 50090118, upload-time = "2026-07-10T08:27:56.051Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8c/faf025357ebf31bc96777f234277aa31e2aeca6dd4ecaa391f29085473c2/pyarrow-25.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d", size = 49945559, upload-time = "2026-07-10T08:28:01.927Z" }, + { url = "https://files.pythonhosted.org/packages/07/a1/bd051871708ea99a5e0fc711926c26c6f2c6d0130c7aaac8093e34998af6/pyarrow-25.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1", size = 53114238, upload-time = "2026-07-10T08:28:08.594Z" }, + { url = "https://files.pythonhosted.org/packages/7c/31/737f0c3cffcd6af647849477d1dd68045deac2e3963c3f9f211bedc48540/pyarrow-25.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18", size = 27861162, upload-time = "2026-07-10T08:28:12.975Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/581ccbcdb3d897eb2893328d68db3d52eca373bf2a7e964d0a6276b8e85b/pyarrow-25.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b", size = 35878945, upload-time = "2026-07-10T08:28:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/64/d1/ccb01db7329ea0411ef4fbd9b62a04d3268b36777d4e758d5e39b91ddeab/pyarrow-25.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b", size = 37630854, upload-time = "2026-07-10T08:28:23.452Z" }, + { url = "https://files.pythonhosted.org/packages/af/9f/2d81ba89d1e4198d0cb25fe7529de936830fdaec0db926bb52a1ef7080d4/pyarrow-25.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec", size = 46905617, upload-time = "2026-07-10T08:28:29.376Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/0ed312ec800fb536f93783215126cee4b8977dcfeccba6f0f44df0cc87d7/pyarrow-25.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887", size = 50119765, upload-time = "2026-07-10T08:28:35.826Z" }, + { url = "https://files.pythonhosted.org/packages/ca/88/cab5063ba0c4d46a9f6b4b7eb1c9029dc0302d65cd5ab3510c949a386568/pyarrow-25.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62", size = 50027563, upload-time = "2026-07-10T08:28:43.848Z" }, + { url = "https://files.pythonhosted.org/packages/7b/fb/4d24f1b7fe2e042dc4ef315ef75e4e702d8e46fe10c37e63caff00502b03/pyarrow-25.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f", size = 53162437, upload-time = "2026-07-10T08:28:52.819Z" }, + { url = "https://files.pythonhosted.org/packages/fa/65/da20806de93ca6ee91e72cb6a9b08b3ac890b46efc8d94a7326c651c4c81/pyarrow-25.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104", size = 28613262, upload-time = "2026-07-10T08:29:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/86/9f/c632afb1d3ef4a7814cee236718235f3a47eac46e97eb87df40f550b6b48/pyarrow-25.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e", size = 36120702, upload-time = "2026-07-10T08:28:59.577Z" }, + { url = "https://files.pythonhosted.org/packages/36/0a/093d53a0e72ad06e45d6443e00651bbc2d21af4211295086cbf4d873d3b9/pyarrow-25.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2", size = 37750674, upload-time = "2026-07-10T08:29:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/8a/18/b37fc31a69cff4bdfb8842683def5612f551b93fff6f44375e4a4a6a5535/pyarrow-25.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50", size = 46912304, upload-time = "2026-07-10T08:29:14.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/35/5cae19ba72493e5598022468b56f6a5571f399f485bf412f157356476caa/pyarrow-25.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe", size = 50073652, upload-time = "2026-07-10T08:29:22.467Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a5/ddd508424bdfd5e6945765e9e2ffc687e2f6115972badc8ecf423076c407/pyarrow-25.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc", size = 50058654, upload-time = "2026-07-10T08:29:29.689Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a4/324d0db203ff5eebe8694ec2d6ec5a23f9aaa5d02e5b8c692914c518c33c/pyarrow-25.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0", size = 53140153, upload-time = "2026-07-10T08:29:36.041Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8d/d236e9c82fe315f9128885c8be3ec719f41965a1eb6b6f4b42470904cd41/pyarrow-25.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849", size = 28743657, upload-time = "2026-07-10T08:29:42.742Z" }, ] [[package]] @@ -1596,6 +1639,120 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/b2/23f4032cd1c9744aa8e9ecda43cd4d755fcb209f7f40fae035248f31a679/pyct-0.6.0-py3-none-any.whl", hash = "sha256:cfaded7289fca72ddf6579b81459e3ec8db323a508e61c49aa318ee3cd6ff160", size = 16630, upload-time = "2025-09-26T08:26:19.092Z" }, ] +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + +[[package]] +name = "pydantic-zarr" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/57/aac9e500abe15719b7406d55871fc8787e7d0ebf83de5f98d44471139f70/pydantic_zarr-0.10.0.tar.gz", hash = "sha256:996b5b4225fb05757e7fff831ee7a75c61f54514a4c2493c7b79635bade7b415", size = 80374, upload-time = "2026-04-23T14:23:12.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/05/04af0ca9c5747c95591a549509ebbb0a986155015de59f1e4c984e3af9ab/pydantic_zarr-0.10.0-py3-none-any.whl", hash = "sha256:d15769dab43346b070051fdf6e83509d7f55259dd00c6d12be2400935117e230", size = 52809, upload-time = "2026-04-23T14:23:11.011Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -1607,45 +1764,27 @@ wheels = [ [[package]] name = "pyogrio" -version = "0.12.1" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "numpy" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/d4/12f86b1ed09721363da4c09622464b604c851a9223fc0c6b393fb2012208/pyogrio-0.12.1.tar.gz", hash = "sha256:e548ab705bb3e5383693717de1e6c76da97f3762ab92522cb310f93128a75ff1", size = 303289, upload-time = "2025-11-28T19:04:53.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/e0/656b6536549d41b5aec57e0deca1f269b4f17532f0636836f587e581603a/pyogrio-0.12.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:7a0d5ca39184030aec4cde30f4258f75b227a854530d2659babc8189d76e657d", size = 23661857, upload-time = "2025-11-28T19:03:27.744Z" }, - { url = "https://files.pythonhosted.org/packages/14/78/313259e40da728bdb60106ffdc7ea8224d164498cb838ecb79b634aab967/pyogrio-0.12.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:feaff42bbe8087ca0b30e33b09d1ce049ca55fe83ad83db1139ef37d1d04f30c", size = 25237106, upload-time = "2025-11-28T19:03:30.018Z" }, - { url = "https://files.pythonhosted.org/packages/8f/ca/5368571a8b00b941ccfbe6ea29a5566aaffd45d4eb1553b956f7755af43e/pyogrio-0.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:81096a5139532de5a8003ef02b41d5d2444cb382a9aecd1165b447eb549180d3", size = 31417048, upload-time = "2025-11-28T19:03:32.572Z" }, - { url = "https://files.pythonhosted.org/packages/ef/85/6eeb875f27bf498d657eb5dab9f58e4c48b36c9037122787abee9a1ba4ba/pyogrio-0.12.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:41b78863f782f7a113ed0d36a5dc74d59735bd3a82af53510899bb02a18b06bb", size = 30952115, upload-time = "2025-11-28T19:03:35.332Z" }, - { url = "https://files.pythonhosted.org/packages/36/f7/cf8bec9024625947e1a71441906f60a5fa6f9e4c441c4428037e73b1fcc8/pyogrio-0.12.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8b65be8c4258b27cc8f919b21929cecdadda4c353e3637fa30850339ef4d15c5", size = 32537246, upload-time = "2025-11-28T19:03:37.969Z" }, - { url = "https://files.pythonhosted.org/packages/ab/10/7c9f5e428273574e69f217eba3a6c0c42936188ad4dcd9e2c41ebb711188/pyogrio-0.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:1291b866c2c81d991bda15021b08b3621709b40ee3a85689229929e9465788bf", size = 22933980, upload-time = "2025-11-28T19:03:41.047Z" }, - { url = "https://files.pythonhosted.org/packages/be/56/f56e79f71b84aa9bea25fdde39fab3846841bd7926be96f623eb7253b7e1/pyogrio-0.12.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:ec0e47a5a704e575092b2fd5c83fa0472a1d421e590f94093eb837bb0a11125d", size = 23658483, upload-time = "2025-11-28T19:03:43.567Z" }, - { url = "https://files.pythonhosted.org/packages/66/ac/5559f8a35d58a16cbb2dd7602dd11936ff8796d8c9bf789f14da88764ec3/pyogrio-0.12.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:b4c888fc08f388be4dd99dfca5e84a5cdc5994deeec0230cc45144d3460e2b21", size = 25232737, upload-time = "2025-11-28T19:03:45.92Z" }, - { url = "https://files.pythonhosted.org/packages/59/58/925f1c129ddd7cbba8dea4e7609797cea7a76dbc863ac9afd318a679c4b9/pyogrio-0.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:73a88436f9962750d782853727897ac2722cac5900d920e39fab3e56d7a6a7f1", size = 31377986, upload-time = "2025-11-28T19:03:48.495Z" }, - { url = "https://files.pythonhosted.org/packages/18/5f/c87034e92847b1844d0e8492a6a8e3301147d32c5e57909397ce64dbedf5/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b5d248a0d59fe9bbf9a35690b70004c67830ee0ebe7d4f7bb8ffd8659f684b3a", size = 30915791, upload-time = "2025-11-28T19:03:51.267Z" }, - { url = "https://files.pythonhosted.org/packages/46/35/b874f79d03e9f900012cf609f7fff97b77164f2e14ee5aac282f8a999c1b/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0622bc1a186421547660271083079b38d42e6f868802936d8538c0b379f1ab6b", size = 32499754, upload-time = "2025-11-28T19:03:58.776Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c4/705678c9c4200130290b3a104b45c0cc10aaa48fcef3b2585b34e34ab3e1/pyogrio-0.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:207bd60c7ffbcea84584596e3637653aa7095e9ee20fa408f90c7f9460392613", size = 22933945, upload-time = "2025-11-28T19:04:01.551Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e0/d92d4944001330bc87742d43f112d63d12fc89378b6187e62ff3fc1e8e85/pyogrio-0.12.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1511b39a283fa27cda906cd187a791578942a87a40b6a06697d9b43bb8ac80b0", size = 23692697, upload-time = "2025-11-28T19:04:04.208Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d7/40acbe06d1b1140e3bb27b79e9163776469c1dc785f1be7d9a7fc7b95c87/pyogrio-0.12.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:e486cd6aa9ea8a15394a5f84e019d61ec18f257eeeb642348bd68c3d1e57280b", size = 25258083, upload-time = "2025-11-28T19:04:07.121Z" }, - { url = "https://files.pythonhosted.org/packages/87/a1/39fefd9cddd95986700524f43d3093b4350f6e4fc200623c3838424a5080/pyogrio-0.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3f1a19f63bfd1d3042e45f37ad1d6598123a5a604b6c4ba3f38b419273486cd", size = 31368995, upload-time = "2025-11-28T19:04:09.88Z" }, - { url = "https://files.pythonhosted.org/packages/18/d7/da88c566e67d741a03851eb8d01358949d52e0b0fc2cd953582dc6d89ff8/pyogrio-0.12.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:f3dcc59b3316b8a0f59346bcc638a4d69997864a4d21da839192f50c4c92369a", size = 31035589, upload-time = "2025-11-28T19:04:12.993Z" }, - { url = "https://files.pythonhosted.org/packages/11/ac/8f0199f0d31b8ddbc4b4ea1918df8070fdf3e0a63100b898633ec9396224/pyogrio-0.12.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a0643e041dee3e8e038fce69f52a915ecb486e6d7b674c0f9919f3c9e9629689", size = 32487973, upload-time = "2025-11-28T19:04:16.103Z" }, - { url = "https://files.pythonhosted.org/packages/bd/64/8541a27e9635a335835d234dfaeb19d6c26097fd88224eda7791f83ca98d/pyogrio-0.12.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5881017f29e110d3613819667657844d8e961b747f2d35cf92f273c27af6d068", size = 22987374, upload-time = "2025-11-28T19:04:18.91Z" }, - { url = "https://files.pythonhosted.org/packages/f4/6f/b4d5e285e08c0c60bcc23b50d73038ddc7335d8de79cc25678cd486a3db0/pyogrio-0.12.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5a1b0453d1c9e7b03715dd57296c8f3790acb8b50d7e3b5844b3074a18f50709", size = 23660673, upload-time = "2025-11-28T19:04:21.662Z" }, - { url = "https://files.pythonhosted.org/packages/8d/75/4b29e71489c5551aa1a1c5ca8c5160a60203c94f2f68c87c0e3614d58965/pyogrio-0.12.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e7ee560422239dd09ca7f8284cc8483a8919c30d25f3049bb0249bff4c38dec4", size = 25232194, upload-time = "2025-11-28T19:04:23.975Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/e9929d2261a07c36301983de2767bcde90d441ab5bf1d767ce56dd07f8b4/pyogrio-0.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:648c6f7f5f214d30e6cf493b4af1d59782907ac068af9119ca35f18153d6865a", size = 31336936, upload-time = "2025-11-28T19:04:26.594Z" }, - { url = "https://files.pythonhosted.org/packages/1d/9e/c59941d734ed936d4e5c89b4b99cb5541307cc42b3fd466ee78a1850c177/pyogrio-0.12.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:58042584f3fd4cabb0f55d26c1405053f656be8a5c266c38140316a1e981aca0", size = 30902210, upload-time = "2025-11-28T19:04:29.143Z" }, - { url = "https://files.pythonhosted.org/packages/d1/68/cc07320a63f9c2586e60bf11d148b00e12d0e707673bffe609bbdcb7e754/pyogrio-0.12.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b438e38e4ccbaedaa5cb5824ff5de5539315d9b2fde6547c1e816576924ee8ca", size = 32461674, upload-time = "2025-11-28T19:04:31.792Z" }, - { url = "https://files.pythonhosted.org/packages/13/bc/e4522f429c45a3b6ad28185849dd76e5c8718b780883c4795e7ee41841ae/pyogrio-0.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:f1d8d8a2fea3781dc2a05982c050259261ebc0f6c5e03732d6d79d582adf9363", size = 23550575, upload-time = "2025-11-28T19:04:34.556Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ac/34f0664d0e391994a7b68529ae07a96432b2b4926dbac173ddc4ec94d310/pyogrio-0.12.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9fe7286946f35a73e6370dc5855bc7a5e8e7babf9e4a8bad7a3279a1d94c7ea9", size = 23694285, upload-time = "2025-11-28T19:04:37.833Z" }, - { url = "https://files.pythonhosted.org/packages/8a/93/873255529faff1da09d0b27287e85ec805a318c60c0c74fd7df77f94e557/pyogrio-0.12.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2c50345b382f1be801d654ec22c70ee974d6057d4ba7afe984b55f2192bc94ee", size = 25259825, upload-time = "2025-11-28T19:04:40.125Z" }, - { url = "https://files.pythonhosted.org/packages/27/95/4d4c3644695d99c6fa0b0b42f0d6266ae9dfaf64478a3371eaac950bdd02/pyogrio-0.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0db95765ac0ca935c7fe579e29451294e3ab19c317b0c59c31fbe92a69155e0", size = 31371995, upload-time = "2025-11-28T19:04:42.736Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6f/71f6bcca8754c8bf55a4b7153c61c91f8ac5ba992568e9fa3e54a0ee76fd/pyogrio-0.12.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fc882779075982b93064b3bf3d8642514a6df00d9dd752493b104817072cfb01", size = 31035498, upload-time = "2025-11-28T19:04:45.79Z" }, - { url = "https://files.pythonhosted.org/packages/fd/47/75c1aa165a988347317afab9b938a01ad25dbca559b582ea34473703dc38/pyogrio-0.12.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:806f620e0c54b54dbdd65e9b6368d24f344cda84c9343364b40a57eb3e1c4dca", size = 32496390, upload-time = "2025-11-28T19:04:48.786Z" }, - { url = "https://files.pythonhosted.org/packages/31/93/4641dc5d952f6bdb71dabad2c50e3f8a5d58396cdea6ff8f8a08bfd4f4a6/pyogrio-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5399f66730978d8852ef5f44dbafa0f738e7f28f4f784349f36830b69a9d2134", size = 23620996, upload-time = "2025-11-28T19:04:51.132Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/de/3c/d2268615e8b749ba59f278b14a495883562e961fa3ad55a9def222bfbd4a/pyogrio-0.13.0.tar.gz", hash = "sha256:9614f27a1891113f80653e0b76b4233ea1fb3beeb1ac46d118ab22e1670f8f13", size = 313103, upload-time = "2026-06-26T15:30:17.375Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/89/76534ad8f01d952ad01002741f8cfac08024035a70952f190b4f7e22325c/pyogrio-0.13.0-cp311-abi3-macosx_12_0_arm64.whl", hash = "sha256:68e6bb9b8b14412311da69679333ad5408c0f9aa5b25d5837bbcba3dfa698109", size = 24666205, upload-time = "2026-06-26T15:29:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/39/58/af3b3a74c8b05ebf49b03303ee24024b9d0272de482867425c8dc93f2820/pyogrio-0.13.0-cp311-abi3-macosx_12_0_x86_64.whl", hash = "sha256:8823f91570c91e66e50cc573bc4722e925b84220ee0c7dc61532438d43c69a95", size = 26063477, upload-time = "2026-06-26T15:29:35.94Z" }, + { url = "https://files.pythonhosted.org/packages/55/30/3e38d8532a33adf15c6465dcd8c1bb2a146dce0da3fd8ba0aa9ec9ba74e4/pyogrio-0.13.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e84e7b09b073ee4cc8c35663afcf644b0c17db75ac72c7591dc3864252db461", size = 32246778, upload-time = "2026-06-26T15:29:40.185Z" }, + { url = "https://files.pythonhosted.org/packages/26/96/888ea83c8d0f1e2cc732bea6be94ed0db784cacd99f0248333483be657b3/pyogrio-0.13.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:680842c88b5e678125edd13b15f7187ff3ce7630cadef538887edd3cbe801287", size = 31670710, upload-time = "2026-06-26T15:29:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/20/c2/247c150f5ca12f8593c20e39115db551b18de5c6cb383006de21b57399e4/pyogrio-0.13.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:220a988ce2a26591d6db5c775b07289d4f54cabdf274cc048f0e17a0b9d5be14", size = 33334097, upload-time = "2026-06-26T15:29:48.533Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ba/3757e312a98c428ac5d8b787f3608ae325174ebef6897930a42e21dd057a/pyogrio-0.13.0-cp311-abi3-win_amd64.whl", hash = "sha256:1b91f6d6e6757a6ea84b9459d24f479dcb52bbf4ebcdb16baf39e49d2836a1cf", size = 23824927, upload-time = "2026-06-26T15:29:52.493Z" }, + { url = "https://files.pythonhosted.org/packages/31/56/5b1bf2637903908a5f7a0e068d602d46f3c03a1f860d40e1528bb5cb7b12/pyogrio-0.13.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:c86c2abade1219863224297f6fdf8b1817c291596b05b865138065a710ea55c3", size = 24741354, upload-time = "2026-06-26T15:29:55.763Z" }, + { url = "https://files.pythonhosted.org/packages/54/5d/1fed0e8f29c457c6b73893bdc66c1c890fd1344539c665f3a8061e4c0f27/pyogrio-0.13.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2548f8b84dae89f5e0cc6d406731f09f234b3909426026428733c21c0a7ac49a", size = 26147175, upload-time = "2026-06-26T15:29:59.156Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c5/1e35904ba332e9e4be83ce4b46e6ef72be05525773717ace0940225932c8/pyogrio-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e605494bfea5d40ad4d37df1db1d7cb8950a3135eff9adba2f79673393f31e12", size = 32648289, upload-time = "2026-06-26T15:30:02.704Z" }, + { url = "https://files.pythonhosted.org/packages/32/dc/50e21c4bc15c504fa72313482d4bf6f39d87195180a53e0e0bc422473592/pyogrio-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dc1d91a2174dc7b4b73b68dc9db124ee5ed35c6f1a1d921b8c3dc79c6e73bc99", size = 32260741, upload-time = "2026-06-26T15:30:06.707Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/313a967cd27f654cee260719dac2c1992b4fe581183a086dffdc785161d7/pyogrio-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:25b0c1a96955c30cd587c024e3e50813ff16a650b4ea41568612842e4078cc59", size = 33840722, upload-time = "2026-06-26T15:30:10.98Z" }, + { url = "https://files.pythonhosted.org/packages/d3/77/5b874829633324c0ae4be45233e0971d8e6e8d9874840940edef315e71e6/pyogrio-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:259cfef6bf5e3060afd5dd00ad5b81175568fc49c6fea7d3be575b7c6feb74fc", size = 24574595, upload-time = "2026-06-26T15:30:14.809Z" }, ] [[package]] @@ -1706,7 +1845,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.1.0" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1715,9 +1854,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -1733,12 +1872,12 @@ wheels = [ ] [[package]] -name = "pytz" -version = "2026.2" +name = "python-dotenv" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] @@ -1826,16 +1965,16 @@ wheels = [ [[package]] name = "s3fs" -version = "2026.4.0" +version = "2026.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiobotocore" }, { name = "aiohttp" }, { name = "fsspec" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/d8/76f3dc1558bdf4494b117a9f7a9cc0a5d9d34edadc9e5d7ceabc5a6a7c37/s3fs-2026.4.0.tar.gz", hash = "sha256:5bdce0abb00b0435ee150807a45fea727451dbc22de4cbc116464f8504ab9d37", size = 85986, upload-time = "2026-04-29T20:52:51.748Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/00/6677343dc919d6c072bb04d80210afdd22c16838a8d16b3315c122dc728f/s3fs-2026.6.0.tar.gz", hash = "sha256:b28de7082d0a4f72392884bdc497e34a4a1582f675d214c7da0acf6e950a0083", size = 87358, upload-time = "2026-06-16T02:05:48.719Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a4/9d1ea10ebc9e028a289a72fec84da170689549a8102c8aacfcad26bc5035/s3fs-2026.4.0-py3-none-any.whl", hash = "sha256:de0d2a1f33cdf03831fd2382d278c6e4e31fe57c3bf2f703c61f8aec6b703e2a", size = 32392, upload-time = "2026-04-29T20:52:50.295Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0b/f68a968b49876eae0f2a515387093cebb2eb9451380a96741cc20efac0d0/s3fs-2026.6.0-py3-none-any.whl", hash = "sha256:60576e31bb31193c1f643f32b4c6439548720ea6918ac702e21cd757c80b5db8", size = 32573, upload-time = "2026-06-16T02:05:47.608Z" }, ] [[package]] @@ -1898,76 +2037,72 @@ wheels = [ [[package]] name = "scipy" -version = "1.17.1" +version = "1.18.0" source = { registry = "https://pypi.org/simple" } 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/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, - { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, - { 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/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, - { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, - { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, - { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, - { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, - { 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/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, - { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, - { 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/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, - { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, - { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, - { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, - { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, - { 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/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, - { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, - { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, - { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, - { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, - { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, ] [[package]] name = "scverse-misc" -version = "0.0.8" +version = "0.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "session-info2" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/2e/2fc93b1c9a7dc3c9c44ba8d239a094d21c8cbf3219a57213fc14a7e9bf28/scverse_misc-0.0.8.tar.gz", hash = "sha256:7de6d46e50fc111d366d60b07f165531de97a836a4d702bbbfa2a47a0015fd9e", size = 31621, upload-time = "2026-06-08T12:01:27.683Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/91/c48708330643569fd14c73c66707a5eae39634fb8f34874bbedcaa3fa996/scverse_misc-0.1.3.tar.gz", hash = "sha256:843f29b40e4bbeab85849dcaa3ce0be1e64dc9de285eb7afff9c249f4c2bc76a", size = 45860, upload-time = "2026-07-18T10:04:16.493Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/58/3c753c43fed0a96db29b787daa1dacd84acc8f484da5dd5f8627601a4e4a/scverse_misc-0.0.8-py3-none-any.whl", hash = "sha256:dc61dbd9a1bdb1171cda9f73c52567e29159d51ff1a0f24bee2e568ca1f96a4a", size = 13955, upload-time = "2026-06-08T12:01:26.639Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ef/cc649ec7ee3b738631f342f678ab78a08e9c481369bf46410677f350d557/scverse_misc-0.1.3-py3-none-any.whl", hash = "sha256:1f0f73b021fd6501800521a47257f640ddf2faf57a37322d133c7b860b6e4813", size = 24854, upload-time = "2026-07-18T10:04:15.263Z" }, +] + +[package.optional-dependencies] +settings = [ + { name = "pydantic-settings" }, + { name = "python-dotenv" }, ] [[package]] @@ -1981,11 +2116,11 @@ wheels = [ [[package]] name = "setuptools" -version = "82.0.1" +version = "83.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, + { 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]] @@ -2082,7 +2217,7 @@ wheels = [ [[package]] name = "spatialdata" -version = "0.7.3" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anndata" }, @@ -2114,23 +2249,42 @@ dependencies = [ { name = "xarray-spatial" }, { name = "zarr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/50/248fae79a62f3d9dffbe70e9b66be6e3f8aaf77caa20a8c931a79256b689/spatialdata-0.7.3.tar.gz", hash = "sha256:6054d66d00e19af96368a897faa0b369f3a25dbf9e0371f091129f8522ee6986", size = 359317, upload-time = "2026-05-06T16:02:42.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/70/e575bc26b0410cf1349f5115b55d8d119edd6b2cf18e32ab84e222ee17e3/spatialdata-0.8.0.tar.gz", hash = "sha256:14143788a1de83302f8e83a29dd723b2d4e82f5b130d840409822fbf3a778e25", size = 369880, upload-time = "2026-07-02T20:42:20.597Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/bd/76cd29c982ed8f06db839411f1f44c521bf36b8714074652368353856b44/spatialdata-0.7.3-py3-none-any.whl", hash = "sha256:15e0df647f149a8be70cef6b866dcf033c71890440583bca8bb1c20c37566e6c", size = 199244, upload-time = "2026-05-06T16:02:40.994Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/f5cac3e58d1bb092bfb6015104e6cce925bbe10a2cae44428c790d4e5d06/spatialdata-0.8.0-py3-none-any.whl", hash = "sha256:75139b0df0e27c4573141a8dbe6b3301b47231571505926ad6a633c046ec1010", size = 202596, upload-time = "2026-07-02T20:42:19.268Z" }, ] [[package]] -name = "spatialdata-codec-writer" +name = "spatialdata-js-util" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "dask", extra = ["array"] }, { name = "imagecodecs" }, { name = "numcodecs" }, { name = "numpy" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "zarr" }, +] + +[package.optional-dependencies] +all = [ + { name = "anndata" }, + { name = "dask", extra = ["array"] }, { name = "ome-zarr" }, + { name = "scipy" }, + { name = "spatialdata" }, + { name = "textual" }, +] +tui = [ + { name = "textual" }, +] +write = [ + { name = "anndata" }, + { name = "dask", extra = ["array"] }, + { name = "ome-zarr" }, + { name = "scipy" }, { name = "spatialdata" }, - { name = "zarr" }, ] [package.dev-dependencies] @@ -2140,14 +2294,21 @@ dev = [ [package.metadata] requires-dist = [ - { name = "dask", extras = ["array"], specifier = ">=2025.0" }, + { name = "anndata", marker = "extra == 'write'", specifier = ">=0.12" }, + { name = "dask", extras = ["array"], marker = "extra == 'write'", specifier = ">=2025.0" }, { name = "imagecodecs", specifier = ">=2025.8.2" }, { name = "numcodecs", specifier = ">=0.16" }, { name = "numpy", specifier = ">=2.0" }, - { name = "ome-zarr", specifier = ">=0.13" }, - { name = "spatialdata", specifier = ">=0.7.2" }, + { name = "ome-zarr", marker = "extra == 'write'", specifier = ">=0.16" }, + { name = "pandas", specifier = ">=2.2" }, + { name = "pyarrow", specifier = ">=18" }, + { name = "scipy", marker = "extra == 'write'", specifier = ">=1.11" }, + { name = "spatialdata", marker = "extra == 'write'", specifier = ">=0.8.0" }, + { name = "spatialdata-js-util", extras = ["write", "tui"], marker = "extra == 'all'" }, + { name = "textual", marker = "extra == 'tui'", specifier = ">=1.0" }, { name = "zarr", specifier = ">=3.1" }, ] +provides-extras = ["write", "tui", "all"] [package.metadata.requires-dev] dev = [{ name = "pytest", specifier = ">=8.0" }] @@ -2161,16 +2322,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl", hash = "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", size = 12893, upload-time = "2025-11-12T12:21:14.407Z" }, ] +[[package]] +name = "textual" +version = "8.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, +] + [[package]] name = "tifffile" -version = "2026.6.1" +version = "2026.7.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/38/5e2ecef5af2f4fd4a89bb8d6240de9458bab4d51a4cbd97aeb3a0cd618e2/tifffile-2026.6.1.tar.gz", hash = "sha256:626c892c0e899d959b9438e7c0e1491dc154a7fead1f1f37a991724a50eceba9", size = 429694, upload-time = "2026-05-31T23:57:12.165Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/2f/e5fe51c8f782241d86fdf7251594b195f0d6c2fcf9d389079de212599246/tifffile-2026.7.14.tar.gz", hash = "sha256:ce2703e5ef22c868f1528d5f5b4ef75eefb019cf628a1c9ec0d17e0afeca8ef5", size = 437660, upload-time = "2026-07-14T23:41:31.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/59/208f71d70ddc6184f79b8c6d87d46eb7d7b12c19186a817dec9c9c3f3693/tifffile-2026.6.1-py3-none-any.whl", hash = "sha256:0d7382d2769b855b81ce358528e2b40c16d48aa39031746efa81215205332a8d", size = 267108, upload-time = "2026-05-31T23:57:10.597Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/d381de4a3cc4e3682cba0338f43250893508aad0b310af1d0635f7b04413/tifffile-2026.7.14-py3-none-any.whl", hash = "sha256:4eb20372e76edf2c9fed922b1e3a0a0567be3560bd2008336115763bb1f3c034", size = 270614, upload-time = "2026-07-14T23:41:30.078Z" }, ] [[package]] @@ -2201,32 +2379,53 @@ wheels = [ [[package]] name = "tqdm" -version = "4.68.2" +version = "4.70.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/05/0d5260f1f1ca784f4a4a0def9cbe6affe587f5b4025328d446c3d67765f4/tqdm-4.68.2.tar.gz", hash = "sha256:89c230e8dbc67c7615c142487111222f878c77427ea09549960f62389e258add", size = 171923, upload-time = "2026-06-09T13:26:42.539Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/75/1a0392bcc21c44dcdf87b3cf2d137e7829be2c083a1e38d44efca3d57a16/tqdm-4.68.2-py3-none-any.whl", hash = "sha256:d4240441fb5353290b87d6a85968c9decc131a99b8c7faa28269d829de669ede", size = 78578, upload-time = "2026-06-09T13:26:40.731Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "tzdata" -version = "2026.2" +version = "2026.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, ] [[package]] @@ -2253,80 +2452,80 @@ wheels = [ [[package]] name = "wrapt" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/0c/bfae7b9401583b6d05938cd16dedc43857d96da2f8a3d50d78cc515bf6ff/wrapt-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ffad790d9d11d8ecf9f17c4bb671a5b4089e4d8b575c46c5129597f41f836b0", size = 81021, upload-time = "2026-05-22T14:48:00.313Z" }, - { url = "https://files.pythonhosted.org/packages/26/58/80f6a6599f933f4caecc1cb3ee88a04faf81e8b9bddbd6109c688dd63e0f/wrapt-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:628f5220c7a904d5fc78f7075c8d7871433eb6d035c94728a22fdf85f193d2a8", size = 81692, upload-time = "2026-05-22T14:48:01.49Z" }, - { url = "https://files.pythonhosted.org/packages/17/93/fb357cc7847c58a8ae790be718903afa81a28d23e642c843dc4129e8a0b2/wrapt-2.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:61acce4257a9883669703c525447c5b4c392edf0f987ae77ec32668440158f0e", size = 169364, upload-time = "2026-05-22T14:48:02.791Z" }, - { url = "https://files.pythonhosted.org/packages/aa/0b/76b601ee309a8bd556af0eecb184394c20b3c49aa9c8e085aa1ffacc2568/wrapt-2.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727ab4244622cd6ad2390f322642090c877d2e83a608d2653a7643ae5368d926", size = 171079, upload-time = "2026-05-22T14:48:04.22Z" }, - { url = "https://files.pythonhosted.org/packages/cd/87/ee3f32d5658e3e26d3e0e457922b47a36dd3bfbdfee7f97bb3e802344a66/wrapt-2.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03df9ebed4c73ab93fa8c07e3d41d818dfca1852b15731a3de59457b27814624", size = 160205, upload-time = "2026-05-22T14:48:05.553Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d0/ae2fd64277a67f5d7bffcf2d05eea1e476263fb2a072baf0b0129ab85984/wrapt-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9ff006f420b2ec8296aa56ade43ea7da3e997e85769f0aafc5e0661aacb710", size = 168922, upload-time = "2026-05-22T14:48:07.132Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f3/2d541a060c5bbafb9400bca4917e4d78bfd1f239f404782c86831a8f6b29/wrapt-2.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:844c858fc3bb7eacc0ba8efa904935d16aac6a4470948ad1e7e55c9f5a2a665f", size = 158388, upload-time = "2026-05-22T14:48:08.629Z" }, - { url = "https://files.pythonhosted.org/packages/1d/68/8d92c8800c57e93cb116ae9e9d6cbafc34fade5ee9f9107b6f203fb4dc35/wrapt-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87bacdaf225117a342a20d9c03438d701c02112f6e3f351ce9b7f32354f14797", size = 167682, upload-time = "2026-05-22T14:48:10.042Z" }, - { url = "https://files.pythonhosted.org/packages/30/72/83ea3790ea352439442349388e29ff07b76e0686265f9088bbb505d1608d/wrapt-2.2.1-cp312-cp312-win32.whl", hash = "sha256:2f8c90c8afde51969487be4e1343ae049b268854877d415c2510baf833775052", size = 77857, upload-time = "2026-05-22T14:48:11.782Z" }, - { url = "https://files.pythonhosted.org/packages/ef/cb/99450668dd3502d62a54a1c8aa56e44f34cb8c1261b381cfe2e7926c3b75/wrapt-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ce32763ac31ce94fe9aada947e479b1975012bff166da409b4b9e4e376cf7e5", size = 80825, upload-time = "2026-05-22T14:48:13.046Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3a/87512881be64e743f9ee4c66f4cbe8e884974bef2a5989af71f999653ac7/wrapt-2.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d1b4d0e0c2119587a31f5c029abd547e0c81d93b89d394566fe1588659eb579", size = 79087, upload-time = "2026-05-22T14:48:14.323Z" }, - { url = "https://files.pythonhosted.org/packages/88/d1/a1b08f8f4fac8cbb156fa51cf64ee2c7f7f74f9875ba3cf70b3c58368694/wrapt-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2beb1c7cab10603aecdc42f8edd6ff013f9a32e4543474e38e6b77ce9975aeb", size = 80831, upload-time = "2026-05-22T14:48:15.598Z" }, - { url = "https://files.pythonhosted.org/packages/54/ce/57890814991446a845e09b3445ce8b694f27eb0577004f2c2a36a9772ed4/wrapt-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0cb7e4dd71f4c32e5e84843cd3c4cd65dda034314004bbe1d7f99af2426ab80", size = 81375, upload-time = "2026-05-22T14:48:17.071Z" }, - { url = "https://files.pythonhosted.org/packages/38/65/08d7a6c76ac4493bdb668205ee9c1de1bd5daca61717c3e9aa49b4c01499/wrapt-2.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95821352042722cd9f1108874579a47989d0a7e12a37d87d2fc4af20fd99ab8a", size = 167417, upload-time = "2026-05-22T14:48:18.303Z" }, - { url = "https://files.pythonhosted.org/packages/62/ce/f1ccbee7a1bfe5cdc6b3da6bab4b45713d628b9294da32a39f563d648140/wrapt-2.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abd621552ede77c4c69be7fac44ba911225b0c812b6ba604e5964cf98085b474", size = 166948, upload-time = "2026-05-22T14:48:19.768Z" }, - { url = "https://files.pythonhosted.org/packages/86/2a/f85d48d1cd4869aee6704028d257d740a47c1c467b457ce396b4b5b55d07/wrapt-2.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e3677c7146ce694874941ba82b57092cc4875445aadf29d72807351023105143", size = 158148, upload-time = "2026-05-22T14:48:21.96Z" }, - { url = "https://files.pythonhosted.org/packages/fe/5c/93939ad11d4a12358ab1aab219a2ef5efa5612e0db6b9fc65af8af1a891b/wrapt-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9a5934eaea872e17936b5f45501eba5ab0bce9a74122e172b663d7c28c459c4a", size = 165905, upload-time = "2026-05-22T14:48:23.373Z" }, - { url = "https://files.pythonhosted.org/packages/e0/22/b8c2aa89862ff58605934d7abf4b70e6a5a1c33df96656f49035ccdf1c8a/wrapt-2.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f5b9daf6b629fce418e0cc3dd0436eac045188fa35deadb7a7f3941d5b8203f9", size = 156712, upload-time = "2026-05-22T14:48:24.767Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/bf00a7b02239c12bb02ddcc3c0b971bfcc36e578c5a44f1ccfef5b458545/wrapt-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f53ac9f3ef573326d009ed809beff4efcac6451931c2b8132586da4b9e53ff31", size = 166560, upload-time = "2026-05-22T14:48:26.83Z" }, - { url = "https://files.pythonhosted.org/packages/fe/93/6390ca9c5b787683cef588d04f57c8d41b9a2323b5597a65f18638c90ef2/wrapt-2.2.1-cp313-cp313-win32.whl", hash = "sha256:1ffa9cfd4bdb581539951b14ae661ff20ed0c3599b3e911a131ee0ec5ac11337", size = 77817, upload-time = "2026-05-22T14:48:28.221Z" }, - { url = "https://files.pythonhosted.org/packages/97/73/ce10f0e71c0cfaa1a65faadb8efd4852028b3bb9ba28932b8889df769d38/wrapt-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:368eac1e20fd0bb03dd3cc42bf9887154c3861b60989389ccb5fac032617d215", size = 80736, upload-time = "2026-05-22T14:48:30.139Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4c/89f4a6818fafbbd840330e4fa3873073e1bfc166133a64cac7f8fde7a5e3/wrapt-2.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:c754dafdf5aaf0b401b644a90a30046929a0dd1a536e0ff0ec959a59155d9c7f", size = 79099, upload-time = "2026-05-22T14:48:31.405Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f2/9a8741c46f8c208ac0a45b25ba170bcb4fb72a2781d5fb97dbd7b6be73cb/wrapt-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ed928d0fda15fc0adc8d13305c8b3c0f2fba5b0669950c9e6d019d9162a3b3e8", size = 82802, upload-time = "2026-05-22T14:48:33.307Z" }, - { url = "https://files.pythonhosted.org/packages/9c/0d/e9c855716a3705eef1416456bdf062b60620726fdc59428ff670fc3c60dc/wrapt-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fafb4e739e43544d12cb4abd1605fd4683b6ca6a9ad682b7fd8f4d21973eafa8", size = 83329, upload-time = "2026-05-22T14:48:34.593Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d6/a88f1c13112b7831adac75cea65d8310e0d696d570c8961844c90a57b865/wrapt-2.2.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74d6a0c31472fe5d814917266b9f46495d7c61ed890af08b468acea92fb89a8d", size = 202937, upload-time = "2026-05-22T14:48:35.859Z" }, - { url = "https://files.pythonhosted.org/packages/42/65/e29d54aef06a4d898a5b8a25589a0b3769bde454f922fad8f6f89fbfb650/wrapt-2.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab5be648d5a0b86b7438864f8df3c705a65cef35a2fd3e5561e3e203167e0f27", size = 209997, upload-time = "2026-05-22T14:48:38.153Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/e4454263516cf0e12640912fbca9a83654e424f0a6ddb79f5cd7ce14bf33/wrapt-2.2.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d8f204c8e3a8bf9ece17e0a83d137fd807440977f8a5e762d59306795011440", size = 194856, upload-time = "2026-05-22T14:48:39.69Z" }, - { url = "https://files.pythonhosted.org/packages/de/d0/fe0ee202286afdf4a7f77dd29f195703145764d572aec209c5086e57d924/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d047f6498c973874ba08ac3f97c69a2c4b2211c8de6f4c205f75cb1c9522596e", size = 205654, upload-time = "2026-05-22T14:48:43.456Z" }, - { url = "https://files.pythonhosted.org/packages/23/b6/87d860dfc6460c246af70b1fd5c8b76df77571b42a493459423ded94fd7d/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7a4fdb9326aab4a5a477a1640e5ad786a8495901009d7e7b038371edd23a9d2b", size = 192206, upload-time = "2026-05-22T14:48:44.858Z" }, - { url = "https://files.pythonhosted.org/packages/df/46/3eea8cde077d985f239a38c0257087b8064fd9ee9b1a99e282d2c86da4ef/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8cc5094b08abeae52da9c73c8a32003623be691a5193df2f4e3eac3d557c394", size = 198428, upload-time = "2026-05-22T14:48:46.319Z" }, - { url = "https://files.pythonhosted.org/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562", size = 79448, upload-time = "2026-05-22T14:48:47.901Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53", size = 83021, upload-time = "2026-05-22T14:48:49.201Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e", size = 80295, upload-time = "2026-05-22T14:48:50.479Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a3/11d7f34ebbf3231bc907a3e6d5ee051b14d034c1bc7b65a97d5cc00516df/wrapt-2.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f56a647e4eaf5f0ca40330fb070f566bdf9f7b0db89a1af20d71c28dcd7a0ab", size = 80879, upload-time = "2026-05-22T14:48:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/13/3c/b74cfd984cef560b900fb1a727af20352d89e1f06bf2e1114dd3f00f5f5a/wrapt-2.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:64b7deeda4b70408e382328d8bbe52a256fe9bc63ae3db86d804608367e5422c", size = 81462, upload-time = "2026-05-22T14:48:53.18Z" }, - { url = "https://files.pythonhosted.org/packages/15/a3/7c8f704b8dc07dfe0a5d01c2edbfd88317aa8e5e3fa7c743eb7a085ae767/wrapt-2.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9cf53ba90717db2e292401de290776c498d4bbfb0d4a559ca2895db8b9dcb5c", size = 167251, upload-time = "2026-05-22T14:48:54.562Z" }, - { url = "https://files.pythonhosted.org/packages/80/85/a34d1888d97247da6c2ff6118c3a721c73ed8cc4dd198c00208bb73b6f80/wrapt-2.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf3638274ab9d9b724c9baa0b4c04e132cd6faefb78b4dd3dd1a02a4bdaad41e", size = 166316, upload-time = "2026-05-22T14:48:56.065Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d7/72ffaeb01eebc704afe3fb99e840480f4bda45f0fa66e3381b6a39251c8f/wrapt-2.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aed9658797d0b45d6c49adcfc6b41f66e6f2d0c6de3ec79e16cf4b1855df240f", size = 157952, upload-time = "2026-05-22T14:48:57.924Z" }, - { url = "https://files.pythonhosted.org/packages/24/5b/36f5d6b024e4edfdd90b140742d11ebcf7836daf5c9daf326c55c24db412/wrapt-2.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d676ee388bc42a04d56dd7deb5605244dac2e35cc2fadbb43c9fa25bbd93508", size = 166130, upload-time = "2026-05-22T14:48:59.384Z" }, - { url = "https://files.pythonhosted.org/packages/81/06/9296d9e97bfdef5483dfcc859d57b095b257144b2bc5300ab521e06f4bc7/wrapt-2.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e395f7bc31851ef9b612050368cb446e9bc14cd7454b025018980349caf25ae5", size = 156604, upload-time = "2026-05-22T14:49:00.921Z" }, - { url = "https://files.pythonhosted.org/packages/53/37/16953929ed6776175720e58fc966e779926d8d71e2c7b2273230590ca71f/wrapt-2.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f1845c2a8cc1180ccccfa45785dd06f562730d19ef75be180334254012b6283", size = 166007, upload-time = "2026-05-22T14:49:02.332Z" }, - { url = "https://files.pythonhosted.org/packages/b9/73/20ee58c0612dae7c31131a7095345812ed2c7b389019e175f68cde34e5b4/wrapt-2.2.1-cp314-cp314-win32.whl", hash = "sha256:436addbc4bb4fc0a88c702577f51195d7d73683a7f3e0e5b253d8404d7847243", size = 78327, upload-time = "2026-05-22T14:49:03.722Z" }, - { url = "https://files.pythonhosted.org/packages/22/b3/ef7c3295d02e0448a71c639a36a057f46d524d057c9486291a7a3039e65c/wrapt-2.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:50972a1d974ea07725a7f6b1cec5f8759008afd030a0024843ebe7d52de47f2b", size = 81144, upload-time = "2026-05-22T14:49:05.093Z" }, - { url = "https://files.pythonhosted.org/packages/ac/dc/7bdf336953f99f4ceb0a584bb8870e42c8f26f93ea10c87834dad62f1668/wrapt-2.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1c9934ea5d92957e3cd0adbc0845539dccfd62710ebe16195a8c66c53954db36", size = 79569, upload-time = "2026-05-22T14:49:06.413Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6d/6dfae80150ff1919c356d1dd528f049bcdfaae29b4d284bc957e022caef4/wrapt-2.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17de18fc12cea55b8a9587314cb830573e37fb33b247a7515696350863714188", size = 82892, upload-time = "2026-05-22T14:49:07.925Z" }, - { url = "https://files.pythonhosted.org/packages/82/7b/4e34766a7d7804ffce9e71befe47e9b3225dc350c49c94493c4ab39fd3a5/wrapt-2.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9dec1aca52dddde7df94818310fa2fe79739c8f385b2014c4cb1035f5508199", size = 83333, upload-time = "2026-05-22T14:49:09.257Z" }, - { url = "https://files.pythonhosted.org/packages/9d/57/0b34db3e8de44ccfece62d7b337abd1631dd810f5adc5f3db571727836b5/wrapt-2.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69f2e9244542cb34dd59c7f073445b9e54ad9f3fce8d93606c368a1b499fc413", size = 202899, upload-time = "2026-05-22T14:49:10.572Z" }, - { url = "https://files.pythonhosted.org/packages/e5/45/ac0c459f154b99d92789a6cba7ca727185b83513b986f8ec7fe2aacddcbf/wrapt-2.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d83966dc7f4f45e8b97b5933685ac2e6e67fc0e19246ea314bceb9a8970c956", size = 209986, upload-time = "2026-05-22T14:49:12.229Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e4/77e37ff33ad018fa81ade52c25fa327b80b56f81d734279a63614fcb4cbc/wrapt-2.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78b0aa6bfb7be8deed0ab23e7aa028cc5210c29bc2d32a04d52b50e517a7307e", size = 194893, upload-time = "2026-05-22T14:49:14.139Z" }, - { url = "https://files.pythonhosted.org/packages/dd/9d/7ea651d1ab032fc5fa222fbec91d0f8a1397f6ae04ebb93fa7219aa921d7/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:05d5cb74d1b232ec8cfa130a8f900708699ff2491d97b8f85a4cdc5996294b85", size = 205636, upload-time = "2026-05-22T14:49:15.714Z" }, - { url = "https://files.pythonhosted.org/packages/09/af/8e88031a701275b9085c54e64bc88c0b1cd55c77eadd400691c371cd76c4/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f6518b94edb9150452e9aba08027d4cc293433753ec1fbefb4629a21cbc74181", size = 192267, upload-time = "2026-05-22T14:49:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/bf/a8/e657ca876b06710194f243d81c4b0896ade646e244bdbec2d87c8c56a8bd/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed55af48b3eb28f43228ca2306788892bcb629eb2b5c4876e2a3659872c2f17a", size = 198378, upload-time = "2026-05-22T14:49:18.785Z" }, - { url = "https://files.pythonhosted.org/packages/c8/59/822efe4ea722a3961331bfa35b7d90937790d2c20f0616de1997ccc3aebd/wrapt-2.2.1-cp314-cp314t-win32.whl", hash = "sha256:2e08688ab16525897da6589d56d0aebaf417bbe91c2d8e3b96203b1efa596e85", size = 80226, upload-time = "2026-05-22T14:49:20.264Z" }, - { url = "https://files.pythonhosted.org/packages/ab/31/2a7dc5f6abb2fca0b6e1610e120419f603650aceb4f1d3ac4cae0354e162/wrapt-2.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fd0135d34387f5fd087d9be368ea77ea89cf2451dc1cd1c622d35021bcb3ab50", size = 83835, upload-time = "2026-05-22T14:49:21.634Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c0/782b86e28d1ceebeb74cccea12d2cd3d2ba0bd68e3dec20b1bc5873f6127/wrapt-2.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f70db64e8266d7c45d3b735f2e08eeb434b5e03da9a479ae42b2e2e486a21a00", size = 80722, upload-time = "2026-05-22T14:49:23.59Z" }, - { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" }, +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/b0/c1f5a970721f06b85c0cd5142e0ff8fe067708abd779b0c4f4be7d61d09f/wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107", size = 131509, upload-time = "2026-07-28T06:06:14.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/4a/d17a0fad1bf1c5f2c887ff71fef75654141b0880bff71d157d955b5bec3a/wrapt-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525", size = 82139, upload-time = "2026-07-28T06:04:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/51b92daaf6defb57f4dc56bdcce985400f75c6984a03ca5e78ccac717028/wrapt-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d", size = 82723, upload-time = "2026-07-28T06:04:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/28/7f/cfd9bc4b1f5e424eeea83d0493e43f3b1b02707ce8e50c47945873982bd5/wrapt-2.3.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8", size = 172381, upload-time = "2026-07-28T06:04:37.674Z" }, + { url = "https://files.pythonhosted.org/packages/cb/89/ff7814f6eb6856b479946117d1138a2fbb46cdb6b1f379db359056c69743/wrapt-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb", size = 174120, upload-time = "2026-07-28T06:04:38.987Z" }, + { url = "https://files.pythonhosted.org/packages/12/1e/8eded8615d39e3ce81f626937a3a87b280a2a86239a2bf14a4b4bb345034/wrapt-2.3.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60", size = 163035, upload-time = "2026-07-28T06:04:40.361Z" }, + { url = "https://files.pythonhosted.org/packages/35/ea/a0af2d9da62897af2a055484920de05dade30d2ba2c0d65cbdea875d3d8b/wrapt-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02", size = 171887, upload-time = "2026-07-28T06:04:41.614Z" }, + { url = "https://files.pythonhosted.org/packages/7e/dd/63cd4c864c65ef4906df64bd2d378f4a62b54f28063f282dfb3bf93caead/wrapt-2.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3", size = 161113, upload-time = "2026-07-28T06:04:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ee/82f1fc9e431b5c2c5a6d201aa865dbeae3984c311c6d11a185f0c8367cf6/wrapt-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d", size = 170530, upload-time = "2026-07-28T06:04:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/37/a5/5dc590e863a419930d988f8b7ca3e75a6befcfb10b6003b3a152f3d5f732/wrapt-2.3.0-cp312-cp312-win32.whl", hash = "sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1", size = 78323, upload-time = "2026-07-28T06:04:45.484Z" }, + { url = "https://files.pythonhosted.org/packages/51/f9/4a6925a07951df56394f7e6ebe14f69f1c5ef9d87aa63e0839acf15aa63a/wrapt-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8", size = 81180, upload-time = "2026-07-28T06:04:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4f/8b5de0395b2a72216751d41c9861df6facaeb611b619d8810ed2b3b23eb2/wrapt-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab", size = 80155, upload-time = "2026-07-28T06:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6e/0f88a072483e76b881e3fdcd6b6ffb4a5791002514fe541e72b1b73c859a/wrapt-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f", size = 81960, upload-time = "2026-07-28T06:04:49.622Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ff/b7e2776e7c294075eb712cc9ef573d1b818f393006d09787262b8fc871c4/wrapt-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f", size = 82435, upload-time = "2026-07-28T06:04:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/343bb5d0f1f9669bc252a6073f085b4abf862511bd5c9c9eaec754341f1d/wrapt-2.3.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5", size = 170350, upload-time = "2026-07-28T06:04:52.187Z" }, + { url = "https://files.pythonhosted.org/packages/59/f8/13b79a392930bd0dd6b86cbfbfe1c40944110456e1dc6d809e5c46ece904/wrapt-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0", size = 170022, upload-time = "2026-07-28T06:04:53.599Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/4f1b6918f5290db959d6e0c07f77385d87cede29c39c9cf8f145e9c82954/wrapt-2.3.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609", size = 161043, upload-time = "2026-07-28T06:04:54.936Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/45d3cf74414780bdff6d0380467e003f6eb0f028b6c9403db868dbc7209c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8", size = 168576, upload-time = "2026-07-28T06:04:56.261Z" }, + { url = "https://files.pythonhosted.org/packages/f3/73/2fa58dd97f191c997755e2c6d569a68f0c433db4e4b36099bdd7227b6cac/wrapt-2.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae", size = 159140, upload-time = "2026-07-28T06:04:57.754Z" }, + { url = "https://files.pythonhosted.org/packages/29/a8/08a56e2000a8816d449dcbad8c8b081697acbbd490821ceca0f9d8e8d20c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3", size = 169263, upload-time = "2026-07-28T06:04:59.161Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d4/354e1725e35a73b2af4fa70a3e024c7a5d1bf1802dfb862dcb668aae0253/wrapt-2.3.0-cp313-cp313-win32.whl", hash = "sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f", size = 78241, upload-time = "2026-07-28T06:05:00.507Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7e/34c87fa2174848dfee820322aaa318bab08913998ccecc8d2f57b4ad4639/wrapt-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838", size = 81113, upload-time = "2026-07-28T06:05:01.839Z" }, + { url = "https://files.pythonhosted.org/packages/11/86/fcc9a530579e008c9478bb565a6cdfbfd33536660f069c8b91a6607c5050/wrapt-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579", size = 80182, upload-time = "2026-07-28T06:05:03.152Z" }, + { url = "https://files.pythonhosted.org/packages/96/50/3864848b95b28ef73e17551fc8dccbff2628a834f52cf26a57f9c419fb83/wrapt-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944", size = 83921, upload-time = "2026-07-28T06:05:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4c/3d1921a60c3e8c71c540ff136e6a47a1fbccf7f671e818394889f7871d9c/wrapt-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360", size = 84412, upload-time = "2026-07-28T06:05:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/fa/1a/4a796ff7adb26ada6d4b758c94d47a38320b085e7099afc088efbbcdb006/wrapt-2.3.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614", size = 207168, upload-time = "2026-07-28T06:05:07.256Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3e/d7777776806c579b761bac2f91721dda9f04c7a1b380213c5935cc750ae6/wrapt-2.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a", size = 214351, upload-time = "2026-07-28T06:05:08.945Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/2d64d394df7bf181955b3bb562bf33c4492fb4be113f53071106d43ad8b5/wrapt-2.3.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687", size = 199020, upload-time = "2026-07-28T06:05:10.418Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3d/fb31d3db7d9834d265fb1a27a2adf0ddf51557c67458c97b22439ad6ae3d/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570", size = 209969, upload-time = "2026-07-28T06:05:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/8724b5da582e62070dc9bf4d8bf1972f317297eefd7ba1f2b5c6393ccf6c/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41", size = 196324, upload-time = "2026-07-28T06:05:13.557Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/3d9ef411149543016ee6bcf3af707f787cebd946527452b94bf122e9b7b4/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4", size = 202610, upload-time = "2026-07-28T06:05:15.048Z" }, + { url = "https://files.pythonhosted.org/packages/13/9b/4fc042ceb757866dd4a5fc057b3b736f2b360d3703ce9f830d83dc9226e0/wrapt-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3", size = 79178, upload-time = "2026-07-28T06:05:16.469Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/b94878f8eed809ca042685276bcea9f24e8c2ca7c9653bb80bbb920a68a5/wrapt-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98", size = 82634, upload-time = "2026-07-28T06:05:18.026Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/663e1de5332a71685a729754312d327d4cada767c36e1c5a2db4c8de49e6/wrapt-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6", size = 81387, upload-time = "2026-07-28T06:05:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/58/10/b073beaea89bc0d3670a75ff51139430a54b6af7ba7796507730634536dd/wrapt-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc", size = 81978, upload-time = "2026-07-28T06:05:21.133Z" }, + { url = "https://files.pythonhosted.org/packages/b3/31/0916d9cebf848ed3f1a0c1888faee421747df77331e4db2bc527a9a85988/wrapt-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1", size = 82518, upload-time = "2026-07-28T06:05:22.562Z" }, + { url = "https://files.pythonhosted.org/packages/f5/73/31c1bf0f3384062751c2094dadb314916d70aa9b6bfd26d994b4a7b393fa/wrapt-2.3.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945", size = 170187, upload-time = "2026-07-28T06:05:23.904Z" }, + { url = "https://files.pythonhosted.org/packages/ed/25/fce087d54b79b8905f3c3c9dd5f454bbd8d8acb80b960c4a6aee5b4659b3/wrapt-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5", size = 169288, upload-time = "2026-07-28T06:05:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/0d09e6dddc6b7a7230ac77f50254b5980ab4fcd22976f72f8cc8a0404458/wrapt-2.3.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3", size = 160932, upload-time = "2026-07-28T06:05:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/0913af0d2ec0c43865d32d615f518fea66c13c5c930e489e9b0de248e9a8/wrapt-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07", size = 169017, upload-time = "2026-07-28T06:05:28.501Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f2/3d1e47ea81b822210f5df1bf942fd90780a75c055243d569b664529dea88/wrapt-2.3.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f", size = 159065, upload-time = "2026-07-28T06:05:30.01Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/ef2066ced8e5fca204e2b361e9708e36555b40949c583d997ea3b590817d/wrapt-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23", size = 168821, upload-time = "2026-07-28T06:05:31.649Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/016104650d4e572fa91506eb396b3dd8efbccc9284fdc1c9479c3d21db28/wrapt-2.3.0-cp314-cp314-win32.whl", hash = "sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b", size = 78700, upload-time = "2026-07-28T06:05:33.391Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/6fdc20a9f2ca304748b3f0819cbf377d55260562777bf0b615431bc3c181/wrapt-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d", size = 81422, upload-time = "2026-07-28T06:05:34.774Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a4/9cbd53bf05746bea2c392af39cb052427a8ec95cbd494d930733d8f44681/wrapt-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab", size = 80639, upload-time = "2026-07-28T06:05:36.228Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/6c5e4a0f66ea0d2b2dd267e8dd05a0014eea56840b3c8595d40b0a5d1f91/wrapt-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84", size = 84030, upload-time = "2026-07-28T06:05:37.714Z" }, + { url = "https://files.pythonhosted.org/packages/6a/eb/a1aedf03283bc9cbf8a1783995ddc54e3c5a86878f19002d2c428494f4c5/wrapt-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7", size = 84419, upload-time = "2026-07-28T06:05:39.131Z" }, + { url = "https://files.pythonhosted.org/packages/63/61/50d511c0dc5105563849e86daa3e16ac7feef699f79fb05af45ea70107d5/wrapt-2.3.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2", size = 207171, upload-time = "2026-07-28T06:05:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/3f/59/9b538cf7795217e810699d16bc88b96a830d9b5c403eb2ec2db6b5f2ae81/wrapt-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c", size = 214329, upload-time = "2026-07-28T06:05:42.287Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/9935d62b1499e5c8b3d191e99ba4eb31ca237a0b699142011a837e9dc7ea/wrapt-2.3.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295", size = 199079, upload-time = "2026-07-28T06:05:43.958Z" }, + { url = "https://files.pythonhosted.org/packages/2b/01/4446b80fa2ffa47a3449b250d004ba1c1937f07f64a179608fec735df866/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd", size = 209992, upload-time = "2026-07-28T06:05:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/d4/07/56f26c9f9979586a021e8148747004aba4498f49458c90b0502969b904e1/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df", size = 196334, upload-time = "2026-07-28T06:05:47.608Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/6d7bcc895b0f28b2250e10908f060687b9165429dcd7f22ddb3d4c031b74/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109", size = 202644, upload-time = "2026-07-28T06:05:49.183Z" }, + { url = "https://files.pythonhosted.org/packages/cd/25/7860927edba06b758b8852a6f02e832be715563c67a6795d94350bc81099/wrapt-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501", size = 79685, upload-time = "2026-07-28T06:05:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0f/270bafe92fde3b069a39bc01e39ee79340895b335640df861d43d2a51885/wrapt-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5", size = 83104, upload-time = "2026-07-28T06:05:52.405Z" }, + { url = "https://files.pythonhosted.org/packages/55/b3/af176d79a8515a8a720eccdad9a96f6e31a30abf2865430c8c42adf2fd13/wrapt-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51", size = 81774, upload-time = "2026-07-28T06:05:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/00/39/3daf9f47be208606586de4568ba6713db53ebc8fd7a575aea1fe57983b69/wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2", size = 61866, upload-time = "2026-07-28T06:06:12.9Z" }, ] [[package]] name = "xarray" -version = "2026.4.0" +version = "2026.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "packaging" }, { name = "pandas" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/a6/6fe936a798a3a38a79c7422d1a31afd2e9a14690fcb0ccff96bc01f04bf2/xarray-2026.4.0.tar.gz", hash = "sha256:c4ac9a01a945d90d5b1628e2af045099a9d4943536d4f2ee3ae963c3b222d15b", size = 3132311, upload-time = "2026-04-13T19:45:36.688Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/96/3f7bdd00e505ec3698903415b30135024703e017b28c6b61f98da3193b0d/xarray-2026.7.0.tar.gz", hash = "sha256:361b495928fdbf5b58d0969bb6775339019da5e93ca74d61ddf4eb5edd6ce604", size = 3145348, upload-time = "2026-07-09T17:38:26.515Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl", hash = "sha256:d43751d9fb4a90f9249c30431684f00c41bc874f1edccd862631a40cbc0edf08", size = 1414326, upload-time = "2026-04-13T19:45:34.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/5b/28365212062939d213802e5e5fe855cdb231e1c2a93254ba1690066504e3/xarray-2026.7.0-py3-none-any.whl", hash = "sha256:bf9dd130b93806dc78e90c1b7ac24851b31557b888674c53622235691cf21824", size = 1426778, upload-time = "2026-07-09T17:38:24.224Z" }, ] [[package]] @@ -2345,7 +2544,7 @@ wheels = [ [[package]] name = "xarray-spatial" -version = "0.10.7" +version = "0.10.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numba" }, @@ -2355,91 +2554,91 @@ dependencies = [ { name = "xarray" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/9c/998a502cf8b58682de23cf8fd67d100e18652eb1407b46058a6db46fe39e/xarray_spatial-0.10.7.tar.gz", hash = "sha256:094bf02a69ea8ae763eebc371a8f46aa172918dba14f1988deb8fd5a57c2c495", size = 6451998, upload-time = "2026-06-14T04:51:29.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/36/4cd4140e22308701b99477951428298a1808b21260de02439a7ed8a55b39/xarray_spatial-0.10.17.tar.gz", hash = "sha256:1c8f689aba774fe0e3bb63c0c30d70c2f25218f57c6fc0531e3e79b49f80a48f", size = 6476202, upload-time = "2026-07-17T19:14:18.951Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/4f/a55c69f05e36e2d69d4a0dee18257bd67de8295bdd6780977bb8e7828888/xarray_spatial-0.10.7-py3-none-any.whl", hash = "sha256:e91734f80c0beff210cfeec30db1cdf4b94cf9d45a673f5ad97c08f06ffe478a", size = 6316670, upload-time = "2026-06-14T04:51:27.494Z" }, + { url = "https://files.pythonhosted.org/packages/c1/98/80f1eb371b5b053c0edec957ad10fbf1038a4206b9e25fbd4c5ebe11d5cc/xarray_spatial-0.10.17-py3-none-any.whl", hash = "sha256:7d0da80fc1563020dec37c74da810ca34d93b773a38f971348408c0cfc8685c3", size = 6549061, upload-time = "2026-07-17T19:14:16.973Z" }, ] [[package]] name = "yarl" -version = "1.24.2" +version = "1.24.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, - { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, - { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, - { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, - { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, - { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, - { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, - { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, - { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, - { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, - { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, - { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, - { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, - { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, - { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, - { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, - { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, - { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, - { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, - { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, - { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, - { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, - { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, - { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, - { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, - { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, - { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, - { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, - { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, - { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, - { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, - { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, - { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, - { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, - { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, - { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, - { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, - { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, - { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, - { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, - { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, - { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, - { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, + { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, + { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, + { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, + { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, + { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, + { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, + { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, + { url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" }, + { url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" }, + { url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" }, + { url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" }, + { url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" }, + { url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" }, + { url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" }, + { url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" }, + { url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, ] [[package]] diff --git a/scripts/vendor-openjph-for-python.mjs b/scripts/vendor-openjph-for-python.mjs index fe22d673..86763d73 100644 --- a/scripts/vendor-openjph-for-python.mjs +++ b/scripts/vendor-openjph-for-python.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Copy openjph-wasm dist assets into spatialdata-codec-writer package data for + * Copy openjph-wasm dist assets into spatialdata-js-util package data for * standalone pip installs (no monorepo checkout required). * * Vendors `dist/index.js` and the whole `dist/wasm/` directory so the package's @@ -31,9 +31,10 @@ const distDir = join(resolveOpenJphRoot(), 'dist'); const vendorRoot = join( repoRoot, 'python', - 'spatialdata-codec-writer', + 'spatialdata-js-util', 'src', - 'spatialdata_codec_writer', + 'spatialdata_js_util', + 'codecs', 'vendor', 'openjph' ); diff --git a/tests/integration/codecFixtures.test.ts b/tests/integration/codecFixtures.test.ts index 29f71b67..3aa74255 100644 --- a/tests/integration/codecFixtures.test.ts +++ b/tests/integration/codecFixtures.test.ts @@ -31,7 +31,7 @@ function ensureCodecFixture() { } mkdirSync(uvCacheDir, { recursive: true }); execSync( - `node scripts/vendor-openjph-for-python.mjs && uv run --directory python/spatialdata-codec-writer python scripts/generate_codec_fixtures.py --output-dir ${JSON.stringify(fixtureDir)} --experimental-htj2k --overwrite`, + `node scripts/vendor-openjph-for-python.mjs && uv run --directory python/spatialdata-js-util --extra write python scripts/generate_codec_fixtures.py --output-dir ${JSON.stringify(fixtureDir)} --experimental-htj2k --overwrite`, { cwd: projectRoot, env: { From 829d64361c8375e739ab06083c48fcde1302c087 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 28 Jul 2026 11:28:00 +0100 Subject: [PATCH 02/10] Update docs and READMEs for spatialdata-js-util Point the monorepo docs, ADR 0002, the shapes plan, and the zarrextra README at the consolidated package and its grouped CLI, and write a PyPI-facing README covering the reader shim, the backend probe, and CSC. Co-Authored-By: Claude Opus 5 --- .../0002-spatially-aware-vector-loading.md | 2 +- docs/docs/vis/codec-fixtures.mdx | 14 +- .../plans/shapes-nonblocking-tiled-loading.md | 4 +- packages/zarrextra/README.md | 2 +- python/spatialdata-js-util/README.md | 222 ++++++++++++++++++ .../docs/htj2k-wasm-encode-design.md | 14 +- scripts/encode-htj2k-plane.mjs | 2 +- 7 files changed, 241 insertions(+), 19 deletions(-) create mode 100644 python/spatialdata-js-util/README.md diff --git a/docs/adr/0002-spatially-aware-vector-loading.md b/docs/adr/0002-spatially-aware-vector-loading.md index 669d204f..8384f646 100644 --- a/docs/adr/0002-spatially-aware-vector-loading.md +++ b/docs/adr/0002-spatially-aware-vector-loading.md @@ -113,7 +113,7 @@ silently swap sort order under the existing `morton-points` format id. Generate comparable permutations with: ```bash -spatialdata-experimental-writer write-index-permutations SOURCE_ZARR DEST_ZARR +spatialdata-js-util points index-permutations SOURCE_ZARR DEST_ZARR ``` The derivative store includes sibling `points//` elements and diff --git a/docs/docs/vis/codec-fixtures.mdx b/docs/docs/vis/codec-fixtures.mdx index b2434dbe..0f0e44af 100644 --- a/docs/docs/vis/codec-fixtures.mdx +++ b/docs/docs/vis/codec-fixtures.mdx @@ -52,15 +52,15 @@ The vis dev script starts the fixture server on port `38473` and proxies ## Python reference writer -The **publishable** package (`spatialdata-codec-writer`) exposes recompression +The **publishable** package (`spatialdata-js-util`) exposes recompression only. Codec test fixtures are generated by repo-local scripts under -`python/spatialdata-codec-writer/scripts/`. +`python/spatialdata-js-util/scripts/`. Useful commands: ```bash pnpm test:fixtures:generate:codecs -uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer inspect ../../test-fixtures/codecs/jpeg2k.manifest.json +uv run --directory python/spatialdata-js-util spatialdata-js-util images inspect ../../test-fixtures/codecs/jpeg2k.manifest.json ``` ### Fixture provenance @@ -91,13 +91,13 @@ SpatialData store, rewrites configured image rasters with JP2K or experimental HTJ2K, and writes labels with Blosc/zstd by default. ```bash -uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer recompress input.sdata.zarr output-jp2k.zarr --image-key morphology_focus --preset balanced --chunks auto --overwrite +uv run --directory python/spatialdata-js-util spatialdata-js-util images recompress input.sdata.zarr output-jp2k.zarr --image-key morphology_focus --preset balanced --chunks auto --overwrite ``` Custom HTJ2K quality (instead of a preset name): ```bash -uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer recompress \ +uv run --directory python/spatialdata-js-util spatialdata-js-util images recompress \ input.sdata.zarr output-htj2k.zarr \ --image-key morphology_focus \ --codec experimental.openjph_htj2k \ @@ -139,7 +139,7 @@ uses OpenJPH WASM (`experimental.openjph_htj2k`) via `HTJ2KEncoder.setQuality(reversible, quality)`. The quality argument is a float quantization factor (lower = higher fidelity, larger output) — not JP2K-style 0–100. Presets map to `balanced: 0.0002`, `small: 0.001` (calibrated roughly on -Xenium morphology; see `python/spatialdata-codec-writer/docs/htj2k-wasm-encode-design.md`). +Xenium morphology; see `python/spatialdata-js-util/docs/htj2k-wasm-encode-design.md`). Override with CLI `--quality 0.001` or per-image JSON `"quality": 0.001` (implies lossy unless `"reversible": true`). A future codec-demo UI may add interactive `q` exploration on sample regions. @@ -162,7 +162,7 @@ considered supported by the current JavaScript decoder paths. Useful Xenium morphology experiment: ```bash -uv run --directory python/spatialdata-codec-writer spatialdata-codec-writer recompress /path/to/input.zarr /path/to/output.zarr --image-key morphology_focus --preset balanced --chunks auto --overwrite +uv run --directory python/spatialdata-js-util spatialdata-js-util images recompress /path/to/input.zarr /path/to/output.zarr --image-key morphology_focus --preset balanced --chunks auto --overwrite ``` When serving recompressed stores for browser experiments, disable HTTP caching diff --git a/docs/plans/shapes-nonblocking-tiled-loading.md b/docs/plans/shapes-nonblocking-tiled-loading.md index 5fa77d8a..c91837cf 100644 --- a/docs/plans/shapes-nonblocking-tiled-loading.md +++ b/docs/plans/shapes-nonblocking-tiled-loading.md @@ -83,7 +83,7 @@ a *consequence* of moving the load into the layer, not a feature bolted on. | Worker decode + transferables | `core/src/workers/points-worker.ts`, `pointsWorkerClient.ts` (`decodeGeometryWithFeaturesInWorker`), `pointsWorkerProtocol.ts` | add a `decode-shapes-wkb` request + handler + client, or a `shapes-worker.ts` | | Row-group range reads (INHERITED — reuse as-is) | `core/src/models/VTableSource.ts` | — | | Tiling metadata + real bounds loader | `core/src/pointsTiling.ts`, `VPointsSource.loadPointsInBounds` (morton bisect) | `ShapesTilingMetadata`, `VShapesSource.loadShapesInBounds` (bbox bisect) | -| Python spatial writer | `python/spatialdata-experimental-writer/src/.../points.py` (morton sort, sentinels, row groups) | shapes GeoParquet writer under `shapes.experimental//` | +| Python spatial writer | `python/spatialdata-js-util/src/.../points.py` (morton sort, sentinels, row groups) | shapes GeoParquet writer under `shapes.experimental//` | Note: the live vis points path currently sets `experimentalOptimizations: 'off'` (`PointsRendererAdapter.ts`), so even the points `TileLayer` is test-exercised but @@ -299,7 +299,7 @@ artifact (Phase 2); renaming the worker (future). ### Phase 2 — the tiled artifact + tiled loader -8. **Python writer** under `python/spatialdata-experimental-writer/`, a +8. **Python writer** under `python/spatialdata-js-util/`, a `shapes` subcommand cloning `points.py` but: DuckDB Hilbert sort on geometry, GeoParquet 1.1 bbox covering column, zstd, sized row groups. Target `shapes.experimental//` (ADR 0002 — standard readers can't consume a diff --git a/packages/zarrextra/README.md b/packages/zarrextra/README.md index 772438f4..ae453b7f 100644 --- a/packages/zarrextra/README.md +++ b/packages/zarrextra/README.md @@ -82,7 +82,7 @@ chunk's `[..., z, y, x]` layout. The `mandelbulb` test fixture exercises this: each chunk is a single codestream spanning 4 z-planes (`(1, 1, 4, 128, 128)`). For offline encode (fixtures, recompress), use `encodeHtj2kPlane()` or -`createOpenJphEncoder()` from the same package. Python `spatialdata-codec-writer` +`createOpenJphEncoder()` from the same package. Python `spatialdata-js-util` uses vendored OpenJPH WASM with a persistent Node worker pool; new stores use codec id `experimental.openjph_htj2k`. diff --git a/python/spatialdata-js-util/README.md b/python/spatialdata-js-util/README.md new file mode 100644 index 00000000..2c792ce6 --- /dev/null +++ b/python/spatialdata-js-util/README.md @@ -0,0 +1,222 @@ +# spatialdata-js-util + +Python utilities for producing — and reading back — SpatialData stores tuned for +incremental reading in the browser by +[SpatialData.js](https://github.com/Taylor-CCB-Group/SpatialData.js). + +Three optimizations, all aimed at making a viewport cost a few small ranged reads +instead of a whole-element download: + +| Area | What it does | Why | +|------|--------------|-----| +| **images** | Recompress rasters as JPEG 2000 or HTJ2K | Tiles decode in the browser; HTJ2K is markedly cheaper to decode than JPEG 2000 | +| **points** | Morton-sort Points Parquet with sentinel bbox rows and controlled row groups | A viewport maps to a few Parquet row-group reads | +| **tables** | Convert `X`/layers from CSR to CSC | Reading one gene becomes one contiguous range read, not a scan of every row | + +Installing the package also **registers the image codecs with zarr**, so stores +written here open in ordinary `spatialdata.read_zarr` — see +[Reading stores back in Python](#reading-stores-back-in-python). + +> **Status: experimental.** The HTJ2K codec identifier is not a standard +> OME-Zarr codec. Stores using it are readable by the `SpatialData.js` runtime +> and by Python with this package installed. It is published in the hope that, +> if useful, the approach can be adopted more broadly. + +## Install + +```bash +pip install spatialdata-js-util +``` + +That base install is enough to **read** stores. To **write** them, add the +`write` extra, which pulls in `spatialdata`, `anndata`, `dask` and `scipy`: + +```bash +pip install 'spatialdata-js-util[write]' +``` + +Requires Python 3.12+ and `spatialdata >= 0.8.0`. + +### Reading stores back in Python + +```python +import spatialdata as sd + +sdata = sd.read_zarr("store-htj2k.zarr") # HTJ2K/JP2K images decode transparently +``` + +No import of this package and no `register_codecs()` call is needed — the codecs +are advertised through `zarr.codecs` entry points, so zarr finds them as soon as +the distribution is installed. (`spatialdata_js_util.codecs.register_codecs()` +exists only for running from a source tree with no installed metadata.) + +### HTJ2K backends, and why there is a probe + +HTJ2K encode/decode can go through either of two OpenJPH builds: + +- **`imagecodecs`** — native OpenJPH, pure `pip install`, no Node.js. Preferred. +- **`openjph-wasm`** — the vendored WASM the JS reader uses, driven by Node.js + worker processes. Requires `node` on `PATH`. + +A WASM build previously used in this project silently decoded *every* component +of a multi-component codestream as component 0 — losing volumetric planes with +no error (see [`docs/multi-component-codec-findings.md`](docs/multi-component-codec-findings.md)). +Because that failure is silent, a backend is not trusted on the strength of its +library name. `imagecodecs` is admitted only if it decodes a committed +multi-component codestream — produced by the WASM encoder — to exactly the +expected samples. Any mismatch, and it is rejected in favour of the WASM path. + +Check what your environment resolved to: + +```bash +spatialdata-js-util codecs info +``` + +Pin a backend explicitly with `SPATIALDATA_JS_UTIL_HTJ2K_BACKEND=imagecodecs` +or `=openjph-wasm` (the probe still gates `imagecodecs`). + +## Images + +Recompress a store, preserving everything that is not an image. Path sources are +copied first, so tables, shapes, points and unconfigured rasters survive without +the whole object being loaded. + +```bash +spatialdata-js-util images recompress input.zarr output-htj2k.zarr \ + --codec experimental.openjph_htj2k --preset balanced --chunks auto --overwrite +``` + +With `--sibling`, each original image is kept and a compressed copy added +alongside it (`morphology_focus:htj2k_balanced`), so tools without the codec keep +working. + +### Presets and `--quality` + +`--quality` is the OpenJPH quantization step: **lower means higher fidelity and a +larger codestream** — not a JPEG-style 0–100 scale. + +| Preset | JPEG 2000 | HTJ2K | +|--------|-----------|-------| +| `lossless` | reversible, round-trip verified | `reversible=True` | +| `balanced` | `level=100` | `quality=0.0002` | +| `small` | `level=75` | `quality=0.001` | + +```bash +spatialdata-js-util images recompress input.zarr out.zarr \ + --codec experimental.openjph_htj2k --quality 0.0005 --sibling --overwrite +``` + +Labels are written with Blosc/zstd. Browser image codecs support `uint8`, +`int8`, `uint16` and `int16` only. A sidecar manifest records the expanded +config, per-raster stats, the codec backend used, and decoded checksums: + +```bash +spatialdata-js-util images inspect output-htj2k.manifest.json +``` + +For repeatable multi-image runs, use a JSON config: + +```json +{ + "default_image": { "codec": "imagecodecs_jpeg2k", "preset": "lossless", "chunks": "auto" }, + "images": { + "morphology_focus": { "preset": "balanced" }, + "he_image": { "codec": "experimental.openjph_htj2k", "quality": 0.001 } + }, + "default_labels": { "codec": "blosc", "clevel": 5 } +} +``` + +## Points + +Morton-sorted Points Parquet, Vitessce-compatible: + +- `morton_code_2d` added using 16 bits per axis +- the first 2–4 rows are sentinels with `morton_code_2d == 0` encoding the full + point bounding box, so readers can get bounds without a scan +- `{feature_key}_codes` added when the element declares a `feature_key` +- string/categorical columns moved to the right of the table +- row-group size controlled, so a viewport maps to a few row-group reads + +```bash +# List Points elements +spatialdata-js-util points list ~/data/xenium.zarr + +# Morton-sort transcripts in place on points//points.parquet +spatialdata-js-util points morton-from-zarr ~/data/xenium.zarr --points-key transcripts + +# Morton-sort a CSV or Parquet file +spatialdata-js-util points morton input.csv output.parquet --feature-key feature_name +``` + +Morton v1 belongs on the **canonical** element path `points//points.parquet`. +Use `--experimental` only for layouts standard readers cannot consume (see +[ADR 0002](../../docs/adr/0002-spatially-aware-vector-loading.md)). + +`points index-permutations` builds a derivative store with several sort layouts +side by side for benchmarking, and `spatialdata-js-util tui` wraps the Points +commands in a Textual UI (`pip install 'spatialdata-js-util[tui]'`). + +## Tables + +```bash +# Rewrite every table's X (and named layers) as CSC, in place +spatialdata-js-util tables to-csc ~/data/xenium.zarr + +# Or write a converted copy, leaving the source alone +spatialdata-js-util tables to-csc input.zarr output-csc.zarr --overwrite +``` + +The conversion changes only the sparse layout, not the values, and the result +stays readable by any AnnData client. Dense matrices are left alone unless you +pass `--densify`, since sparsifying a dense matrix can make it bigger. + +```python +from spatialdata_js_util import convert_store_tables_to_csc, to_csc + +convert_store_tables_to_csc("store.zarr") # whole store +csc = to_csc(adata.X) # or just a matrix +``` + +## Python API + +```python +from spatialdata_js_util import ( + recompress_spatialdata, + write_morton_points_parquet, + convert_store_tables_to_csc, + backend_report, +) + +result = recompress_spatialdata( + "input.zarr", "output-htj2k.zarr", + codec="experimental.openjph_htj2k", + preset="balanced", + overwrite=True, +) +print(result.manifest_path) +``` + +## Monorepo development + +From the repository root: + +```bash +pnpm test:python +``` + +The vendored `openjph-wasm` assets under +`src/spatialdata_js_util/codecs/vendor/openjph/` are gitignored and produced by +`node scripts/vendor-openjph-for-python.mjs`, which `pnpm test:python` runs +first. They are only needed for the WASM backend; the `imagecodecs` backend works +without them. + +Fixture-generation scripts under `scripts/` are repo-local and not shipped in the +wheel. To regenerate the backend probe fixture (requires Node.js): + +```bash +uv run --directory python/spatialdata-js-util python scripts/build_htj2k_probe.py +``` + +TypeScript callers can use `encodeHtj2kPlane()` from `zarrextra` directly; see +[`docs/htj2k-wasm-encode-design.md`](docs/htj2k-wasm-encode-design.md). diff --git a/python/spatialdata-js-util/docs/htj2k-wasm-encode-design.md b/python/spatialdata-js-util/docs/htj2k-wasm-encode-design.md index 107d8716..e2f4990c 100644 --- a/python/spatialdata-js-util/docs/htj2k-wasm-encode-design.md +++ b/python/spatialdata-js-util/docs/htj2k-wasm-encode-design.md @@ -1,7 +1,7 @@ # HTJ2K OpenJPH WASM encode Status: **implemented** — HTJ2K encode uses vendored OpenJPH WASM inside a pool -of persistent Node.js workers (`spatialdata_codec_writer/vendor/encode-plane.mjs`). +of persistent Node.js workers (`spatialdata_js_util/codecs/vendor/encode-plane.mjs`). New stores are labelled `experimental.openjph_htj2k`. Native `imagecodecs` HTJ2K encode is intentionally **not** used: PyPI wheels are @@ -36,7 +36,7 @@ back to planar `[..., z, y, x]`. The `mandelbulb` fixture uses ## Encode flow ```text -Python spatialdata-codec-writer +Python spatialdata-js-util → EncoderPool (N persistent Node workers) → vendored encode-plane.mjs --worker → openjph-wasm encode({ data, width, height, components, reversible, quality }) @@ -46,7 +46,7 @@ Python spatialdata-codec-writer Vendoring: run `node scripts/vendor-openjph-for-python.mjs` at the monorepo root to copy `openjph-wasm` dist assets (`index.mjs` + `wasm/`) into the Python package wheel. The copied `vendor/openjph/` blobs are gitignored; CI and -`pnpm test:codec-writer` run the vendor step after `pnpm install`. +`pnpm test:python` run the vendor step after `pnpm install`. Preset mapping: @@ -81,7 +81,7 @@ rather than presets alone. ### CLI and JSON ```bash -spatialdata-codec-writer recompress input.zarr output.zarr \ +spatialdata-js-util images recompress input.zarr output.zarr \ --image-key morphology_focus \ --codec experimental.openjph_htj2k \ --quality 0.001 \ @@ -113,9 +113,9 @@ Mandelbrot plane across several qualities. ## References -- Python encode: [`htj2k_encode.py`](../src/spatialdata_codec_writer/htj2k_encode.py), - [`codecs.py`](../src/spatialdata_codec_writer/codecs.py) -- Vendored Node worker: [`vendor/encode-plane.mjs`](../src/spatialdata_codec_writer/vendor/encode-plane.mjs) +- Python encode: [`htj2k_wasm.py`](../src/spatialdata_js_util/codecs/htj2k_wasm.py), + [`encoding.py`](../src/spatialdata_js_util/codecs/encoding.py) +- Vendored Node worker: [`vendor/encode-plane.mjs`](../src/spatialdata_js_util/codecs/vendor/encode-plane.mjs) - Vendor script: [`scripts/vendor-openjph-for-python.mjs`](../../../scripts/vendor-openjph-for-python.mjs) - Dev fixtures: [`scripts/generate_codec_fixtures.py`](../scripts/generate_codec_fixtures.py) - JS encode: [`packages/zarrextra/src/htj2k-encode.ts`](../../../packages/zarrextra/src/htj2k-encode.ts) diff --git a/scripts/encode-htj2k-plane.mjs b/scripts/encode-htj2k-plane.mjs index 8aa38b3c..51ac0cb5 100644 --- a/scripts/encode-htj2k-plane.mjs +++ b/scripts/encode-htj2k-plane.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Encode one 2D image plane to HTJ2K for Python spatialdata-codec-writer. + * Encode one 2D image plane to HTJ2K for Python spatialdata-js-util. * * stdin: JSON { width, height, dtype, reversible?, quality?, plane: base64 } * stdout: raw HTJ2K bytes From 3b39b9eff33320cd56347048bc21d8575394a21e Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 28 Jul 2026 11:31:27 +0100 Subject: [PATCH 03/10] Remove dead imports surfaced by the move All three were already unused before the consolidation; they only became visible when the moved files were linted. Co-Authored-By: Claude Opus 5 --- python/spatialdata-js-util/scripts/fixture_writer.py | 1 - python/spatialdata-js-util/scripts/write_synthetic.py | 2 +- .../src/spatialdata_js_util/index_permutations.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/python/spatialdata-js-util/scripts/fixture_writer.py b/python/spatialdata-js-util/scripts/fixture_writer.py index f70cf8f2..6b3ab191 100644 --- a/python/spatialdata-js-util/scripts/fixture_writer.py +++ b/python/spatialdata-js-util/scripts/fixture_writer.py @@ -8,7 +8,6 @@ import numpy as np from spatialdata_js_util.codecs import ( - CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K, HTJ2K_ENCODER, CodecName, diff --git a/python/spatialdata-js-util/scripts/write_synthetic.py b/python/spatialdata-js-util/scripts/write_synthetic.py index 3ef66900..21ed31f9 100644 --- a/python/spatialdata-js-util/scripts/write_synthetic.py +++ b/python/spatialdata-js-util/scripts/write_synthetic.py @@ -16,7 +16,7 @@ if str(_SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(_SCRIPTS_DIR)) -from spatialdata_js_util.codecs import CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K, CodecName +from spatialdata_js_util.codecs import CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K from spatialdata_js_util.codecs import htj2k_available from fixture_writer import WrittenFixture, write_codec_spatialdata diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/index_permutations.py b/python/spatialdata-js-util/src/spatialdata_js_util/index_permutations.py index 5e46e9ff..65f0a2dd 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/index_permutations.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/index_permutations.py @@ -6,7 +6,6 @@ from pathlib import Path from typing import Any, Sequence -import pandas as pd from .points import MORTON_CODE_2D_COLUMN, write_morton_points_parquet from .store import ( From 0f338686036a2e23658d9f029949b05da9047d93 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 28 Jul 2026 12:25:16 +0100 Subject: [PATCH 04/10] Extend the TUI to the whole package and document it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TUI predated the consolidation, so it only covered the five Points commands — nothing in it reflected image recompression, CSC tables, or codec backends. Adds `RecompressImagesScreen` and `TablesToCscScreen`, plus table listing and codec-backend info, and regroups the home menu by area (IMAGES/POINTS/TABLES/CODECS). New screens follow the existing form -> confirm -> run -> report flow, so anything that overwrites still stops on a confirmation naming the exact path: converting tables in place, and replacing an existing output store. `VerifyReportScreen` grew a per-command summary rather than the previous rows/output pair, which said nothing useful for a recompression manifest or a CSC report. Tests drive the UI headlessly through real work — filling the forms and pressing the buttons a user would — and assert the store on disk actually changed, including that cancelling a confirm leaves it untouched. The README now leads with the TUI, since it is the easiest entry point and was previously a single buried line in the Points section. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yml | 2 +- package.json | 2 +- python/spatialdata-js-util/README.md | 62 +++- python/spatialdata-js-util/pyproject.toml | 4 + .../src/spatialdata_js_util/cli.py | 10 +- .../src/spatialdata_js_util/tui/models.py | 20 +- .../src/spatialdata_js_util/tui/screens.py | 334 +++++++++++++++++- python/spatialdata-js-util/tests/test_tui.py | 287 +++++++++++++++ python/spatialdata-js-util/uv.lock | 19 +- 9 files changed, 711 insertions(+), 29 deletions(-) create mode 100644 python/spatialdata-js-util/tests/test_tui.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4395b882..c2f33cf4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -129,7 +129,7 @@ jobs: run: pnpm test:fixtures:generate - name: Run spatialdata-js-util tests - run: uv run --directory python/spatialdata-js-util --extra write pytest + run: uv run --directory python/spatialdata-js-util --extra write --extra tui pytest - name: Run unit tests run: pnpm test:unit diff --git a/package.json b/package.json index c6151c75..653f1904 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "test:fixtures:generate:0.7.2": "uv run python/scripts/generate_fixtures.py --version 0.7.2", "test:fixtures:generate:codecs": "node scripts/vendor-openjph-for-python.mjs && UV_CACHE_DIR=.tmp/uv-cache uv run --directory python/spatialdata-js-util --extra write python scripts/generate_codec_fixtures.py --output-dir ../../test-fixtures/codecs --experimental-htj2k --overwrite", "write-synthetic": "node scripts/vendor-openjph-for-python.mjs && uv run --directory python/spatialdata-js-util --extra write python scripts/write_synthetic.py", - "test:python": "node scripts/vendor-openjph-for-python.mjs && UV_CACHE_DIR=.tmp/uv-cache uv run --directory python/spatialdata-js-util --extra write pytest", + "test:python": "node scripts/vendor-openjph-for-python.mjs && UV_CACHE_DIR=.tmp/uv-cache uv run --directory python/spatialdata-js-util --extra write --extra tui pytest", "test:server": "node scripts/test-server.js", "test:proxy": "node scripts/cors-proxy.js", "validate:datasets:js": "node scripts/validate-datasets-js.js", diff --git a/python/spatialdata-js-util/README.md b/python/spatialdata-js-util/README.md index 2c792ce6..4288e0c9 100644 --- a/python/spatialdata-js-util/README.md +++ b/python/spatialdata-js-util/README.md @@ -17,6 +17,9 @@ Installing the package also **registers the image codecs with zarr**, so stores written here open in ordinary `spatialdata.read_zarr` — see [Reading stores back in Python](#reading-stores-back-in-python). +Every command is available three ways: an [interactive TUI](#interactive-tui) +(start here), the `spatialdata-js-util` CLI, and the [Python API](#python-api). + > **Status: experimental.** The HTJ2K codec identifier is not a standard > OME-Zarr codec. Stores using it are readable by the `SpatialData.js` runtime > and by Python with this package installed. It is published in the hope that, @@ -37,6 +40,61 @@ pip install 'spatialdata-js-util[write]' Requires Python 3.12+ and `spatialdata >= 0.8.0`. +## Interactive TUI + +If you would rather not memorise flags, everything below is also available as a +terminal UI. It is the easiest way to get started. + +```bash +pip install 'spatialdata-js-util[tui]' +spatialdata-js-util tui + +# Or pre-fill the store path so you skip the first prompt +spatialdata-js-util tui ~/data/xenium.zarr +``` + +Pick a command from the home menu and the UI walks you through it: + +``` +IMAGES Recompress rasters (JPEG 2000 / HTJ2K) +POINTS List Points elements in a Zarr store +POINTS Morton-sort Points from Zarr +POINTS Morton-sort CSV/Parquet file +POINTS Write multiscale Points Parquet +POINTS Write index permutations derivative store +TABLES List table elements in a Zarr store +TABLES Convert table matrices to CSC +CODECS Show HTJ2K backend availability +``` + +The flow is the same for every command: **guided form → confirm → run → report.** + +- **Forms** are pre-filled with sensible defaults, and with the store path you + already used this session. `Enter` advances between fields and submits on the + last one; `Escape` goes back; `q` quits. +- **Anything that overwrites asks first.** Rewriting `points//points.parquet` + in place, converting tables in place, or replacing an existing output store all + stop on a confirmation screen naming the exact path before touching anything. +- **Runs stream their JSON output** to a log pane, so a long recompression shows + progress rather than appearing to hang. +- **Writes are verified afterwards** and the report screen shows a pass/fail + table. For Morton writes that means: + + | Check | Meaning | + |-------|---------| + | `column_present` | `morton_code_2d` column exists | + | `sentinel_prefix` | First 2–4 rows have `morton_code_2d == 0` | + | `sentinel_bbox` | Sentinel rows encode the full dataset x/y bounds | + | `morton_monotonic` | Morton codes non-decreasing after sentinels | + | `row_group_sentinels` | Row group 0 contains only sentinel rows | + | `no_uint_intermediates` | No persisted `*_uint` staging columns | + + Multiscale and index-permutation runs show schema/manifest checks instead. + +`CODECS → Show HTJ2K backend availability` is a good first stop: it tells you +which codec backend your environment resolved to, and whether it passed the +multi-component probe described [below](#htj2k-backends-and-why-there-is-a-probe). + ### Reading stores back in Python ```python @@ -154,8 +212,8 @@ Use `--experimental` only for layouts standard readers cannot consume (see [ADR 0002](../../docs/adr/0002-spatially-aware-vector-loading.md)). `points index-permutations` builds a derivative store with several sort layouts -side by side for benchmarking, and `spatialdata-js-util tui` wraps the Points -commands in a Textual UI (`pip install 'spatialdata-js-util[tui]'`). +side by side for benchmarking. All of these are also available from the +[TUI](#interactive-tui), which verifies the output after each write. ## Tables diff --git a/python/spatialdata-js-util/pyproject.toml b/python/spatialdata-js-util/pyproject.toml index 3bde1619..a1348434 100644 --- a/python/spatialdata-js-util/pyproject.toml +++ b/python/spatialdata-js-util/pyproject.toml @@ -73,6 +73,9 @@ package = true [tool.pytest.ini_options] testpaths = ["tests"] +# The TUI tests drive Textual's async pilot; they carry explicit +# `@pytest.mark.asyncio` markers, so strict mode is what we want. +asyncio_mode = "strict" [tool.basedpyright] extraPaths = ["scripts"] @@ -80,4 +83,5 @@ extraPaths = ["scripts"] [dependency-groups] dev = [ "pytest>=8.0", + "pytest-asyncio>=0.24", ] diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/cli.py b/python/spatialdata-js-util/src/spatialdata_js_util/cli.py index 8751cff0..97c7c3b6 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/cli.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/cli.py @@ -524,8 +524,14 @@ def _tui(args: argparse.Namespace) -> None: def _add_tui_command(subparsers: argparse._SubParsersAction) -> None: tui = subparsers.add_parser( "tui", - help="interactive terminal workflow for points commands", - description="Launch a Textual workflow UI for the Points writer commands.", + help="interactive terminal UI for every command", + description=( + "Launch the interactive workflow UI. It covers image recompression, " + "Points indexing, table CSC conversion, and codec backend info, with " + "guided forms, confirmation before any in-place write, and post-write " + "verification. Requires the 'tui' extra: " + "pip install 'spatialdata-js-util[tui]'" + ), ) tui.add_argument( "zarr", diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/tui/models.py b/python/spatialdata-js-util/src/spatialdata_js_util/tui/models.py index c2042c49..85bc265e 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/tui/models.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/tui/models.py @@ -7,11 +7,21 @@ class CommandId(str, Enum): - LIST_POINTS = "list-points" - MORTON_FROM_ZARR = "morton-points-from-zarr" - MORTON_POINTS = "morton-points" - MULTISCALE_POINTS = "multiscale-points" - INDEX_PERMUTATIONS = "write-index-permutations" + """Values mirror the `spatialdata-js-util` CLI paths the screens wrap.""" + + # images + RECOMPRESS = "images recompress" + # points + LIST_POINTS = "points list" + MORTON_FROM_ZARR = "points morton-from-zarr" + MORTON_POINTS = "points morton" + MULTISCALE_POINTS = "points multiscale" + INDEX_PERMUTATIONS = "points index-permutations" + # tables + LIST_TABLES = "tables list" + TABLES_TO_CSC = "tables to-csc" + # codecs + CODECS_INFO = "codecs info" VerifyKind = Literal["none", "morton", "multiscale", "manifest"] diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py b/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py index 193d40fe..d24085ad 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import os import traceback from pathlib import Path from typing import Any @@ -22,17 +23,21 @@ ListItem, ListView, RichLog, + Select, Static, ) +from ..codecs import CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K from ..errors import WriterCommandError from ..index_permutations import DEFAULT_CONDITIONS from ..runners import ( resolve_morton_from_zarr_output, run_list_points, + run_list_tables, run_morton_points, run_morton_points_from_zarr, run_multiscale_points, + run_tables_to_csc, run_write_index_permutations, ) from ..verify import ( @@ -109,25 +114,31 @@ def on_screen_resume(self, event: ScreenResume) -> None: def compose(self) -> ComposeResult: yield Header() yield Static( - "SpatialData experimental writer — pick a command.", + "spatialdata-js-util — pick a command.", id="home-title", ) yield ListView( - ListItem(Label("List Points elements in a Zarr store"), id="cmd-list-points"), - ListItem(Label("Morton-sort Points from Zarr"), id="cmd-morton-from-zarr"), - ListItem(Label("Morton-sort CSV/Parquet file"), id="cmd-morton-points"), - ListItem(Label("Write multiscale Points Parquet"), id="cmd-multiscale-points"), + ListItem(Label("IMAGES Recompress rasters (JPEG 2000 / HTJ2K)"), id="cmd-recompress"), + ListItem(Label("POINTS List Points elements in a Zarr store"), id="cmd-list-points"), + ListItem(Label("POINTS Morton-sort Points from Zarr"), id="cmd-morton-from-zarr"), + ListItem(Label("POINTS Morton-sort CSV/Parquet file"), id="cmd-morton-points"), + ListItem(Label("POINTS Write multiscale Points Parquet"), id="cmd-multiscale-points"), ListItem( - Label("Write index permutations derivative store"), + Label("POINTS Write index permutations derivative store"), id="cmd-index-permutations", ), + ListItem(Label("TABLES List table elements in a Zarr store"), id="cmd-list-tables"), + ListItem(Label("TABLES Convert table matrices to CSC"), id="cmd-tables-to-csc"), + ListItem(Label("CODECS Show HTJ2K backend availability"), id="cmd-codecs-info"), id="command-list", ) yield Footer() def on_list_view_selected(self, event: ListView.Selected) -> None: item_id = event.item.id or "" - if item_id == "cmd-list-points": + if item_id == "cmd-recompress": + self.app.push_screen(RecompressImagesScreen()) + elif item_id == "cmd-list-points": self._start_zarr_command(CommandId.LIST_POINTS) elif item_id == "cmd-morton-from-zarr": self._start_zarr_command(CommandId.MORTON_FROM_ZARR) @@ -137,6 +148,19 @@ def on_list_view_selected(self, event: ListView.Selected) -> None: self.app.push_screen(MultiscaleScreen()) elif item_id == "cmd-index-permutations": self.app.push_screen(IndexPermutationsScreen()) + elif item_id == "cmd-list-tables": + self._start_list_tables() + elif item_id == "cmd-tables-to-csc": + self.app.push_screen(TablesToCscScreen()) + elif item_id == "cmd-codecs-info": + self.app.push_screen(RunScreen(codecs_info_task())) + + def _start_list_tables(self) -> None: + zarr = self.app.context.zarr_path + if not zarr: + self.app.push_screen(ZarrPathScreen(CommandId.LIST_TABLES)) + return + self.app.push_screen(RunScreen(list_tables_task(zarr))) def _start_zarr_command(self, command: CommandId) -> None: if command == CommandId.LIST_POINTS: @@ -202,6 +226,9 @@ def _continue(self) -> None: if self.command == CommandId.LIST_POINTS: self._run_list_points(path) return + if self.command == CommandId.LIST_TABLES: + self.app.push_screen(RunScreen(list_tables_task(path))) + return self.app.push_screen(PointsKeyScreen(self.command)) def _run_list_points(self, zarr: str) -> None: @@ -674,6 +701,248 @@ def runner() -> dict[str, Any]: ) +def list_tables_task(zarr: str) -> TaskSpec: + def runner() -> dict[str, Any]: + return run_list_tables(zarr) + + return TaskSpec( + command=CommandId.LIST_TABLES, + title="List tables", + runner=runner, + ) + + +def codecs_info_task() -> TaskSpec: + def runner() -> dict[str, Any]: + from ..codecs.backends import backend_report + + return backend_report() + + return TaskSpec( + command=CommandId.CODECS_INFO, + title="HTJ2K backends", + runner=runner, + ) + + +class RecompressImagesScreen(InputFormScreen): + """Recompress image rasters — the `images recompress` command.""" + + INPUT_ORDER = ("source", "dest", "image-key", "quality", "chunks", "workers") + + def compose(self) -> ComposeResult: + yield Header() + yield Static("Recompress images", classes="screen-title") + with VerticalScroll(): + yield Label("Source store") + yield Input( + value=self.app.context.zarr_path or "", + placeholder="/path/to/input.zarr", + id="source", + ) + yield Label("Destination store") + yield Input(placeholder="/path/to/output.zarr", id="dest") + yield Label("Image key (blank = every image)") + yield Input(placeholder="morphology_focus", id="image-key") + yield Label("Codec") + yield Select( + [ + ("HTJ2K (experimental.openjph_htj2k)", CODEC_HTJ2K_OPENJPH), + ("JPEG 2000 (imagecodecs_jpeg2k)", CODEC_JPEG2K), + ], + value=CODEC_HTJ2K_OPENJPH, + allow_blank=False, + id="codec", + ) + yield Label("Preset") + yield Select( + [("lossless", "lossless"), ("balanced", "balanced"), ("small", "small")], + value="lossless", + allow_blank=False, + id="preset", + ) + yield Label("HTJ2K quality — quantization step, lower = better (blank = use preset)") + yield Input(placeholder="0.0002", id="quality") + yield Label("Chunks") + yield Input(value="auto", placeholder="auto, or one integer per axis", id="chunks") + yield Label("Workers") + yield Input(value=str(os.cpu_count() or 1), id="workers") + yield Checkbox("Write siblings instead of replacing images", id="sibling") + yield Checkbox("Overwrite destination if it exists", id="overwrite") + with Horizontal(): + yield Button("Run", variant="primary", id="run") + yield Button("Back", id="back") + yield Footer() + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "back": + self.action_go_back() + return + self._submit() + + def _submit(self) -> None: + source = self.query_one("#source", Input).value.strip() + dest = self.query_one("#dest", Input).value.strip() + if not source or not dest: + self.notify("Source and destination are required.", severity="error") + return + if not Path(source).is_dir(): + self.notify(f"Not a directory: {source}", severity="error") + return + + codec = str(self.query_one("#codec", Select).value) + preset = str(self.query_one("#preset", Select).value) + quality_raw = self.query_one("#quality", Input).value.strip() + quality: float | None = None + if quality_raw: + if codec != CODEC_HTJ2K_OPENJPH: + self.notify("Quality applies to HTJ2K only.", severity="error") + return + try: + quality = float(quality_raw) + except ValueError: + self.notify("Quality must be a number.", severity="error") + return + + chunks_raw = self.query_one("#chunks", Input).value.strip() or "auto" + if chunks_raw == "auto": + chunks: Any = "auto" + else: + try: + chunks = tuple(int(part) for part in chunks_raw.split()) + except ValueError: + self.notify( + "Chunks must be 'auto' or space-separated integers.", severity="error" + ) + return + + try: + workers = _positive_int(self.query_one("#workers", Input).value, os.cpu_count() or 1) + except ValueError as exc: + self.notify(str(exc), severity="error") + return + + image_key = self.query_one("#image-key", Input).value.strip() or None + sibling = self.query_one("#sibling", Checkbox).value + overwrite = self.query_one("#overwrite", Checkbox).value + self.app.context.zarr_path = source + + def runner() -> dict[str, Any]: + from ..images import recompress_spatialdata + + return recompress_spatialdata( + source, + dest, + image_key=image_key, + codec=codec, + # An explicit quality overrides the preset, so don't send both. + preset=None if quality is not None else preset, + chunks=chunks, + quality=quality, + sibling=sibling, + overwrite=overwrite, + workers=workers, + ).manifest + + dest_exists = Path(dest).exists() + task = TaskSpec( + command=CommandId.RECOMPRESS, + title="Recompress images", + runner=runner, + requires_confirm=dest_exists, + confirm_message=( + f"Destination already exists and will be replaced:\n{dest}\n\nProceed?" + ), + ) + if dest_exists and not overwrite: + self.notify( + "Destination exists — tick 'Overwrite destination' to replace it.", + severity="error", + ) + return + self.app.push_screen(ConfirmScreen(task) if task.requires_confirm else RunScreen(task)) + + +class TablesToCscScreen(InputFormScreen): + """Convert table matrices to CSC — the `tables to-csc` command.""" + + INPUT_ORDER = ("source", "dest", "tables") + + def compose(self) -> ComposeResult: + yield Header() + yield Static("Convert tables to CSC", classes="screen-title") + with VerticalScroll(): + yield Static( + "CSC makes reading one variable (gene) a single contiguous range read " + "instead of a scan of every row. Values are unchanged.", + classes="section-label", + ) + yield Label("Source store") + yield Input( + value=self.app.context.zarr_path or "", + placeholder="/path/to/store.zarr", + id="source", + ) + yield Label("Destination store (blank = rewrite source in place)") + yield Input(placeholder="/path/to/output.zarr", id="dest") + yield Label("Table keys, comma-separated (blank = all tables)") + yield Input(placeholder="table", id="tables") + yield Checkbox("Convert named layers too", value=True, id="layers") + yield Checkbox("Also convert dense matrices (may increase size)", id="densify") + yield Checkbox("Overwrite destination if it exists", id="overwrite") + with Horizontal(): + yield Button("Run", variant="primary", id="run") + yield Button("Back", id="back") + yield Footer() + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "back": + self.action_go_back() + return + self._submit() + + def _submit(self) -> None: + source = self.query_one("#source", Input).value.strip() + if not source: + self.notify("Source store is required.", severity="error") + return + if not Path(source).is_dir(): + self.notify(f"Not a directory: {source}", severity="error") + return + + dest = self.query_one("#dest", Input).value.strip() or None + tables_raw = self.query_one("#tables", Input).value.strip() + tables = [key.strip() for key in tables_raw.split(",") if key.strip()] or None + layers = self.query_one("#layers", Checkbox).value + densify = self.query_one("#densify", Checkbox).value + overwrite = self.query_one("#overwrite", Checkbox).value + self.app.context.zarr_path = source + + def runner() -> dict[str, Any]: + return run_tables_to_csc( + source, + dest, + tables=tables, + layers=layers, + densify=densify, + overwrite=overwrite, + ) + + in_place = dest is None + task = TaskSpec( + command=CommandId.TABLES_TO_CSC, + title="Tables to CSC", + runner=runner, + requires_confirm=in_place, + confirm_message=( + f"Rewrite table matrices in place:\n{source}\n\n" + f"{'All tables' if tables is None else 'Tables: ' + ', '.join(tables)}\n\n" + "Values are preserved; only the sparse layout changes. Proceed?" + ), + ) + self.app.push_screen(ConfirmScreen(task) if in_place else RunScreen(task)) + + class ConfirmScreen(WriterScreen): BINDINGS = [ Binding("enter", "confirm", "Confirm overwrite"), @@ -819,16 +1088,7 @@ def compose(self) -> ComposeResult: if self.error: yield Static(f"Error: {self.error}", id="error-text") elif self.result: - if self.task_spec.command == CommandId.LIST_POINTS: - keys = self.result.get("points_keys", []) - yield Static(f"Points keys: {', '.join(keys) or '(none)'}") - else: - rows = self.result.get("rows") - output = self.result.get("output") - if rows is not None: - yield Static(f"Rows: {rows}") - if output: - yield Static(f"Output: {output}") + yield from self._summary_lines(self.result) if self.checks: passed = all_passed(self.checks) status = "All checks passed" if passed else "Some checks failed" @@ -847,6 +1107,46 @@ def compose(self) -> ComposeResult: yield Button("Quit", id="quit") yield Footer() + def _summary_lines(self, result: dict[str, Any]) -> ComposeResult: + """Render the headline facts per command; the full JSON is on the run log.""" + command = self.task_spec.command + if command == CommandId.LIST_POINTS: + keys = result.get("points_keys", []) + yield Static(f"Points keys: {', '.join(keys) or '(none)'}") + elif command == CommandId.LIST_TABLES: + keys = result.get("table_keys", []) + yield Static(f"Table keys: {', '.join(keys) or '(none)'}") + elif command == CommandId.CODECS_INFO: + yield Static(f"Selected backend: {result.get('selected') or '(none available)'}") + for name, info in (result.get("backends") or {}).items(): + available = "available" if info.get("available") else "not available" + probe = "probe OK" if info.get("passes_multicomponent_probe") else "probe FAILED" + yield Static(f" {name}: {available}, {probe}") + elif command == CommandId.RECOMPRESS: + images = result.get("images") or [] + yield Static(f"Output: {result.get('output')}") + yield Static(f"Rasters recompressed: {len(images)}") + total = sum(int(image.get("encoded_bytes") or 0) for image in images) + if total: + yield Static(f"Encoded bytes: {total:,}") + backend = (result.get("htj2k") or {}).get("selected") + if backend: + yield Static(f"HTJ2K backend: {backend}") + elif command == CommandId.TABLES_TO_CSC: + yield Static(f"Output: {result.get('output')}") + for table in result.get("tables") or []: + matrices = ", ".join( + f"{key} {value}" for key, value in (table.get("matrices") or {}).items() + ) + yield Static(f" {table.get('path')}: {matrices or '(no matrices)'}") + else: + rows = result.get("rows") + output = result.get("output") + if rows is not None: + yield Static(f"Rows: {rows}") + if output: + yield Static(f"Output: {output}") + def on_mount(self) -> None: self.query_one("#home", Button).focus() diff --git a/python/spatialdata-js-util/tests/test_tui.py b/python/spatialdata-js-util/tests/test_tui.py new file mode 100644 index 00000000..13ef5cd5 --- /dev/null +++ b/python/spatialdata-js-util/tests/test_tui.py @@ -0,0 +1,287 @@ +"""Drive the TUI headlessly through real commands. + +These are not snapshot tests: each one fills the form the user would fill, presses +the buttons the user would press, and asserts the store on disk actually changed. +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +import numpy as np +import pytest + +pytest.importorskip("textual") +anndata = pytest.importorskip("anndata") +sparse = pytest.importorskip("scipy.sparse") +sd = pytest.importorskip("spatialdata") + +from textual.widgets import Button, Checkbox, Input, ListView # noqa: E402 + +from spatialdata_js_util.tui.app import WriterApp # noqa: E402 +from spatialdata_js_util.tui.models import CommandId # noqa: E402 +from spatialdata_js_util.tui.screens import ( # noqa: E402 + ConfirmScreen, + HomeScreen, + RecompressImagesScreen, + TablesToCscScreen, + VerifyReportScreen, +) + + +def _store_with_table(path: Path) -> Path: + from spatialdata.models import TableModel + + matrix = sparse.random(24, 6, density=0.4, format="csr", random_state=0, dtype=np.float32) + adata = anndata.AnnData(X=matrix) + adata.obs["region"] = "img" + adata.obs["region"] = adata.obs["region"].astype("category") + adata.obs["instance_id"] = np.arange(24) + table = TableModel.parse( + adata, region="img", region_key="region", instance_key="instance_id" + ) + sd.SpatialData(tables={"table": table}).write(path, overwrite=True) + return path + + +def _store_with_image(path: Path) -> np.ndarray: + from spatialdata.models import Image2DModel + + y, x = np.mgrid[0:64, 0:48] + pixels = np.stack([((x * 9 + y * 5) % 4096).astype(np.uint16)]) + sd.SpatialData( + images={"morphology": Image2DModel.parse(pixels, dims=("c", "y", "x"))} + ).write(path, overwrite=True) + return pixels + + +async def _select(pilot, item_id: str) -> None: + """Activate a home-menu entry by id.""" + menu = pilot.app.screen.query_one("#command-list", ListView) + index = next(i for i, item in enumerate(menu.children) if item.id == item_id) + menu.index = index + await pilot.pause() + menu.action_select_cursor() + await pilot.pause() + + +async def _run_to_report(pilot, timeout: float = 120.0) -> VerifyReportScreen: + """Wait for the report screen that RunScreen pushes when its worker finishes. + + Polled rather than keyed off `workers.wait_for_complete()`: the worker is + started by RunScreen.on_mount, so waiting before the screen has mounted sees + no workers and returns immediately. + """ + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + while loop.time() < deadline: + if isinstance(pilot.app.screen, VerifyReportScreen): + return pilot.app.screen + await pilot.pause() + await asyncio.sleep(0.05) + raise AssertionError( + f"no report screen after {timeout}s; stuck on {type(pilot.app.screen).__name__}" + ) + + +class TestHome: + @pytest.mark.asyncio + async def test_menu_covers_every_command_group(self) -> None: + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + assert isinstance(app.screen, HomeScreen) + ids = [item.id for item in app.screen.query_one("#command-list", ListView).children] + assert ids == [ + "cmd-recompress", + "cmd-list-points", + "cmd-morton-from-zarr", + "cmd-morton-points", + "cmd-multiscale-points", + "cmd-index-permutations", + "cmd-list-tables", + "cmd-tables-to-csc", + "cmd-codecs-info", + ] + + @pytest.mark.asyncio + async def test_codecs_info_reports_a_backend(self) -> None: + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-codecs-info") + report = await _run_to_report(pilot) + assert report.error is None + assert report.result is not None + assert report.task_spec.command is CommandId.CODECS_INFO + assert "backends" in report.result + + @pytest.mark.asyncio + async def test_list_tables_prompts_for_a_store_then_lists(self, tmp_path: Path) -> None: + store = _store_with_table(tmp_path / "s.zarr") + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-list-tables") + # No store in context yet, so the path prompt comes first. + app.screen.query_one("#zarr-path", Input).value = str(store) + await pilot.pause() + app.screen.query_one("#continue", Button).press() + report = await _run_to_report(pilot) + assert report.error is None + assert report.result is not None + assert report.result["table_keys"] == ["table"] + + +class TestTablesToCsc: + @pytest.mark.asyncio + async def test_in_place_conversion_confirms_then_rewrites(self, tmp_path: Path) -> None: + store = _store_with_table(tmp_path / "s.zarr") + assert sd.read_zarr(store).tables["table"].X.format == "csr" + + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-tables-to-csc") + assert isinstance(app.screen, TablesToCscScreen) + + app.screen.query_one("#source", Input).value = str(store) + await pilot.pause() + app.screen.query_one("#run", Button).press() + await pilot.pause() + + # In-place writes must be confirmed before anything is touched. + assert isinstance(app.screen, ConfirmScreen) + assert str(store) in app.screen.task_spec.confirm_message + app.screen.query_one("#confirm", Button).press() + + report = await _run_to_report(pilot) + assert report.error is None, report.error + + assert sd.read_zarr(store).tables["table"].X.format == "csc" + + @pytest.mark.asyncio + async def test_cancelling_the_confirm_leaves_the_store_alone(self, tmp_path: Path) -> None: + store = _store_with_table(tmp_path / "s.zarr") + + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-tables-to-csc") + app.screen.query_one("#source", Input).value = str(store) + await pilot.pause() + app.screen.query_one("#run", Button).press() + await pilot.pause() + assert isinstance(app.screen, ConfirmScreen) + app.screen.query_one("#cancel", Button).press() + await pilot.pause() + + assert sd.read_zarr(store).tables["table"].X.format == "csr" + + @pytest.mark.asyncio + async def test_writing_to_a_destination_skips_the_confirm(self, tmp_path: Path) -> None: + store = _store_with_table(tmp_path / "s.zarr") + dest = tmp_path / "csc.zarr" + + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-tables-to-csc") + app.screen.query_one("#source", Input).value = str(store) + app.screen.query_one("#dest", Input).value = str(dest) + await pilot.pause() + app.screen.query_one("#run", Button).press() + report = await _run_to_report(pilot) + assert report.error is None, report.error + + assert sd.read_zarr(dest).tables["table"].X.format == "csc" + assert sd.read_zarr(store).tables["table"].X.format == "csr" + + @pytest.mark.asyncio + async def test_missing_source_is_reported_not_run(self, tmp_path: Path) -> None: + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-tables-to-csc") + app.screen.query_one("#source", Input).value = str(tmp_path / "nope.zarr") + await pilot.pause() + app.screen.query_one("#run", Button).press() + await pilot.pause() + # Stays on the form rather than launching a doomed run. + assert isinstance(app.screen, TablesToCscScreen) + + +class TestRecompressImages: + @pytest.mark.asyncio + async def test_recompresses_losslessly_and_reads_back(self, tmp_path: Path) -> None: + source = tmp_path / "src.zarr" + pixels = _store_with_image(source) + dest = tmp_path / "htj2k.zarr" + + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-recompress") + assert isinstance(app.screen, RecompressImagesScreen) + + app.screen.query_one("#source", Input).value = str(source) + app.screen.query_one("#dest", Input).value = str(dest) + await pilot.pause() + app.screen.query_one("#run", Button).press() + + report = await _run_to_report(pilot) + assert report.error is None, report.error + assert report.result is not None + assert report.result["images"], "expected at least one recompressed raster" + + read_back = np.asarray(sd.read_zarr(dest).images["morphology"].data.compute()) + assert np.array_equal(read_back, pixels) + + @pytest.mark.asyncio + async def test_quality_is_rejected_for_jpeg2000(self, tmp_path: Path) -> None: + source = tmp_path / "src.zarr" + _store_with_image(source) + + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-recompress") + from textual.widgets import Select + + from spatialdata_js_util.codecs import CODEC_JPEG2K + + app.screen.query_one("#source", Input).value = str(source) + app.screen.query_one("#dest", Input).value = str(tmp_path / "out.zarr") + app.screen.query_one("#codec", Select).value = CODEC_JPEG2K + app.screen.query_one("#quality", Input).value = "0.001" + await pilot.pause() + app.screen.query_one("#run", Button).press() + await pilot.pause() + # HTJ2K-only option on a JPEG 2000 run must not silently be ignored. + assert isinstance(app.screen, RecompressImagesScreen) + + @pytest.mark.asyncio + async def test_existing_destination_requires_the_overwrite_box(self, tmp_path: Path) -> None: + source = tmp_path / "src.zarr" + _store_with_image(source) + dest = tmp_path / "out.zarr" + dest.mkdir() + + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-recompress") + app.screen.query_one("#source", Input).value = str(source) + app.screen.query_one("#dest", Input).value = str(dest) + await pilot.pause() + app.screen.query_one("#run", Button).press() + await pilot.pause() + assert isinstance(app.screen, RecompressImagesScreen) + + # Ticking the box gets past the guard, onto the confirm screen. + app.screen.query_one("#overwrite", Checkbox).value = True + await pilot.pause() + app.screen.query_one("#run", Button).press() + await pilot.pause() + assert isinstance(app.screen, ConfirmScreen) diff --git a/python/spatialdata-js-util/uv.lock b/python/spatialdata-js-util/uv.lock index f3f9e086..fce051cc 100644 --- a/python/spatialdata-js-util/uv.lock +++ b/python/spatialdata-js-util/uv.lock @@ -1859,6 +1859,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2290,6 +2303,7 @@ write = [ [package.dev-dependencies] dev = [ { name = "pytest" }, + { name = "pytest-asyncio" }, ] [package.metadata] @@ -2311,7 +2325,10 @@ requires-dist = [ provides-extras = ["write", "tui", "all"] [package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=8.0" }] +dev = [ + { name = "pytest", specifier = ">=8.0" }, + { name = "pytest-asyncio", specifier = ">=0.24" }, +] [[package]] name = "tblib" From 57254816fd3e775a71e0d794f137bb0683389dee Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 28 Jul 2026 13:54:07 +0100 Subject: [PATCH 05/10] Add multiscale pyramids for single-resolution rasters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Acquisition output is often written at one resolution. A browser then has to fetch full-resolution chunks however far out the user is zoomed, which defeats the image codecs rather than complementing them. Adds `images add-pyramid` and a `--pyramid` option on `images recompress`, so single-resolution input becomes browser-ready in one pass. Levels default to halving until the largest spatial axis fits ~one chunk (1024px); `--pyramid-levels`/`--pyramid-downscale` override that. Images that already have a pyramid are skipped and reported as such unless `--pyramid-force` is passed. Levels go through SpatialData's own parsers rather than hand-written `multiscales` metadata: each level needs a scale *and* a half-pixel translation (s1 by 0.5, s2 by 1.5, ...), and getting those wrong misaligns every level against full resolution. It also means labels are downsampled by the label model rather than averaged, so no level contains an id that identifies no object — verified in the tests. Inside `recompress`, pyramids are staged in a temp store rather than written straight into the destination: `_recompress_image_array` deletes its destination array before streaming the source into it, so reading the new levels from the destination would pull the rug out from under itself. Rebuilding is always copy-then-write. SpatialData refuses to delete the files backing a live element, so there is no in-place mode; `add_pyramids` rejects a destination equal to the source with that explanation. The TUI recompress form carries the same controls, and the report screen shows which rasters were rebuilt or skipped. Co-Authored-By: Claude Opus 5 --- python/spatialdata-js-util/README.md | 50 ++- .../src/spatialdata_js_util/__init__.py | 10 + .../src/spatialdata_js_util/cli.py | 110 ++++++ .../src/spatialdata_js_util/images.py | 97 +++++- .../src/spatialdata_js_util/pyramids.py | 312 ++++++++++++++++++ .../src/spatialdata_js_util/tui/screens.py | 47 +++ .../tests/test_pyramids.py | 265 +++++++++++++++ python/spatialdata-js-util/tests/test_tui.py | 76 ++++- 8 files changed, 958 insertions(+), 9 deletions(-) create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/pyramids.py create mode 100644 python/spatialdata-js-util/tests/test_pyramids.py diff --git a/python/spatialdata-js-util/README.md b/python/spatialdata-js-util/README.md index 4288e0c9..c0688c09 100644 --- a/python/spatialdata-js-util/README.md +++ b/python/spatialdata-js-util/README.md @@ -9,7 +9,7 @@ instead of a whole-element download: | Area | What it does | Why | |------|--------------|-----| -| **images** | Recompress rasters as JPEG 2000 or HTJ2K | Tiles decode in the browser; HTJ2K is markedly cheaper to decode than JPEG 2000 | +| **images** | Recompress rasters as JPEG 2000 or HTJ2K, and add [multiscale pyramids](#multiscale-pyramids) where they are missing | Tiles decode in the browser; HTJ2K is markedly cheaper to decode than JPEG 2000. Without a pyramid, a zoomed-out view still costs full-resolution chunks | | **points** | Morton-sort Points Parquet with sentinel bbox rows and controlled row groups | A viewport maps to a few Parquet row-group reads | | **tables** | Convert `X`/layers from CSR to CSC | Reading one gene becomes one contiguous range read, not a scan of every row | @@ -77,6 +77,8 @@ The flow is the same for every command: **guided form → confirm → run → re stop on a confirmation screen naming the exact path before touching anything. - **Runs stream their JSON output** to a log pane, so a long recompression shows progress rather than appearing to hang. +- The recompress form carries the [pyramid](#multiscale-pyramids) controls, so + single-resolution input can be made browser-ready without leaving the UI. - **Writes are verified afterwards** and the report screen shows a pass/fail table. For Morton writes that means: @@ -164,6 +166,45 @@ spatialdata-js-util images recompress input.zarr out.zarr \ --codec experimental.openjph_htj2k --quality 0.0005 --sibling --overwrite ``` +### Multiscale pyramids + +Acquisition output is often written at a single resolution. A browser then has no +choice but to fetch full-resolution chunks however far out the user is zoomed — +which defeats the image codecs rather than complementing them. `--pyramid` gives +any single-level image a pyramid before recompressing it, so one command turns +single-resolution input into a browser-ready store: + +```bash +spatialdata-js-util images recompress input.zarr output.zarr \ + --codec experimental.openjph_htj2k --preset balanced --pyramid --overwrite +``` + +To add pyramids without touching codecs: + +```bash +spatialdata-js-util images add-pyramid input.zarr output.zarr --overwrite +``` + +By default levels are chosen automatically: keep halving until the largest +spatial axis is `--pyramid-min-size` (1024) or smaller, so the coarsest level is +roughly one chunk. Override with `--pyramid-levels N` (a *total* count including +full resolution) and `--pyramid-downscale K`. + +Images that already have more than one level are **skipped**, and the manifest +says so; `--pyramid-force` rebuilds them anyway. Pass `--labels` to +`add-pyramid` to include label elements — they are downsampled by the label +model, so ids are never averaged into values that identify no object. + +Full resolution is copied through unchanged; only the added levels are new. +Levels are generated through SpatialData's own parsers, which get the per-level +scale *and* half-pixel translation right — hand-written `multiscales` metadata +tends to misalign every level against the full-resolution image. + +Pyramids are always written to a **different** store than the source. SpatialData +refuses to delete the files backing a live element, so there is no in-place mode. + +### Other image notes + Labels are written with Blosc/zstd. Browser image codecs support `uint8`, `int8`, `uint16` and `int16` only. A sidecar manifest records the expanded config, per-raster stats, the codec backend used, and decoded checksums: @@ -241,6 +282,8 @@ csc = to_csc(adata.X) # or just a matrix ```python from spatialdata_js_util import ( recompress_spatialdata, + add_pyramids, + has_pyramid, write_morton_points_parquet, convert_store_tables_to_csc, backend_report, @@ -250,9 +293,14 @@ result = recompress_spatialdata( "input.zarr", "output-htj2k.zarr", codec="experimental.openjph_htj2k", preset="balanced", + pyramid=True, # add levels to single-resolution images first overwrite=True, ) print(result.manifest_path) + +# Or pyramids on their own +has_pyramid("input.zarr", "images", "morphology") # -> False +add_pyramids("input.zarr", "output.zarr", levels=4) ``` ## Monorepo development diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/__init__.py b/python/spatialdata-js-util/src/spatialdata_js_util/__init__.py index bf332491..9e6aa716 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/__init__.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/__init__.py @@ -32,6 +32,12 @@ recompress_spatialdata, resolve_recompression_config, ) +from .pyramids import ( + PyramidResult, + add_pyramids, + has_pyramid, + resolve_scale_factors, +) from .points import ( MORTON_CODE_2D_COLUMN, MORTON_CODE_EXTREME_VALUE_INDICATOR, @@ -56,17 +62,21 @@ "MORTON_CODE_2D_COLUMN", "MORTON_CODE_EXTREME_VALUE_INDICATOR", "CscConversion", + "PyramidResult", "RecompressedSpatialData", + "add_pyramids", "anndata_to_csc", "backend_report", "build_spatialdata_multiscale_metadata", "convert_store_tables_to_csc", + "has_pyramid", "htj2k_available", "is_htj2k_codec", "morton_sort_points", "recompress_spatialdata", "register_codecs", "resolve_recompression_config", + "resolve_scale_factors", "to_csc", "write_morton_points_parquet", "write_multiscale_points_parquet", diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/cli.py b/python/spatialdata-js-util/src/spatialdata_js_util/cli.py index 97c7c3b6..65c97ec4 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/cli.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/cli.py @@ -92,10 +92,93 @@ def _images_recompress(args: argparse.Namespace) -> None: reversible=True if args.reversible else None, sibling=args.sibling, workers=args.workers, + # Any explicit --pyramid-* option means the user wants pyramids, so the + # bare --pyramid flag is a convenience rather than a requirement. + pyramid=args.pyramid + or args.pyramid_levels != "auto" + or args.pyramid_downscale != 2 + or args.pyramid_min_size != 1024 + or args.pyramid_force, + pyramid_levels=args.pyramid_levels, + pyramid_downscale=args.pyramid_downscale, + pyramid_min_size=args.pyramid_min_size, + pyramid_force=args.pyramid_force, ) _print_json(result.manifest) +def _pyramid_levels(value: str) -> int | str: + if value == "auto": + return "auto" + try: + parsed = int(value) + except ValueError: + raise argparse.ArgumentTypeError("--pyramid-levels must be 'auto' or an integer") from None + if parsed < 1: + raise argparse.ArgumentTypeError("--pyramid-levels must be at least 1") + return parsed + + +def _add_pyramid_args(parser: argparse.ArgumentParser, *, with_flag: bool) -> None: + if with_flag: + parser.add_argument( + "--pyramid", + action="store_true", + help=( + "Build a multiscale pyramid for any image that has only one resolution " + "level, before recompressing. Implied by the other --pyramid-* options." + ), + ) + parser.add_argument( + "--pyramid-levels", + type=_pyramid_levels, + default="auto", + metavar="N", + help=( + "Total resolution levels including full res, or 'auto' to halve until the " + "largest spatial axis fits --pyramid-min-size (default: auto)" + ), + ) + parser.add_argument( + "--pyramid-downscale", + type=_positive_int, + default=2, + metavar="K", + help="Downscale factor between levels (default: 2)", + ) + parser.add_argument( + "--pyramid-min-size", + type=_positive_int, + default=1024, + metavar="PX", + help="Stop 'auto' once the largest spatial axis is this size or smaller (default: 1024)", + ) + parser.add_argument( + "--pyramid-force", + action="store_true", + help="Rebuild pyramids for images that already have more than one level", + ) + + +def _images_add_pyramid(args: argparse.Namespace) -> None: + from .pyramids import add_pyramids + + def command() -> dict[str, Any]: + return add_pyramids( + args.source, + args.dest, + image_keys=args.image_key.split(",") if args.image_key else None, + include_labels=args.labels, + levels=args.pyramid_levels, + downscale=args.pyramid_downscale, + min_size=args.pyramid_min_size, + force=args.pyramid_force, + overwrite=args.overwrite, + ).manifest + + _run_command(command) + + def _images_inspect(args: argparse.Namespace) -> None: manifest_path = Path(args.path) if manifest_path.is_dir(): @@ -172,8 +255,35 @@ def _add_images_commands(subparsers: argparse._SubParsersAction) -> None: default=os.cpu_count() or 1, help="Parallel encoder workers (default: CPU count)", ) + _add_pyramid_args(recompress, with_flag=True) recompress.set_defaults(func=_images_recompress) + add_pyramid = image_commands.add_parser( + "add-pyramid", + help="give single-resolution rasters a multiscale pyramid", + description=( + "Copy a store and add multiscale pyramids to rasters that have only one " + "resolution level, so a zoomed-out browser view does not have to read " + "full-resolution chunks. Codecs are left unchanged; use " + "'images recompress --pyramid' to do both in one pass." + ), + ) + add_pyramid.add_argument("source", help="Existing SpatialData Zarr store") + add_pyramid.add_argument("dest", help="Output SpatialData Zarr store") + add_pyramid.add_argument( + "--image-key", + metavar="KEYS", + help="Comma-separated image element names (default: all images)", + ) + add_pyramid.add_argument( + "--labels", + action="store_true", + help="Also build pyramids for labels (downsampled without inventing label ids)", + ) + add_pyramid.add_argument("--overwrite", action="store_true") + _add_pyramid_args(add_pyramid, with_flag=False) + add_pyramid.set_defaults(func=_images_add_pyramid) + inspect = image_commands.add_parser( "inspect", help="print a recompression manifest", diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/images.py b/python/spatialdata-js-util/src/spatialdata_js_util/images.py index 6b0e0a05..098983d0 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/images.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/images.py @@ -32,6 +32,7 @@ ) from .codecs.htj2k_wasm import configure_encoder_pool from .provenance import package_version, sha256 +from .pyramids import DEFAULT_MIN_SIZE as DEFAULT_PYRAMID_MIN_SIZE from .store import read_json, refresh_consolidated_metadata, write_json ImagePreset = Literal["lossless", "balanced", "small"] @@ -541,6 +542,44 @@ def _sibling_image_key(key: str, codec: str, preset: str) -> str: return f"{key}:{_codec_sibling_suffix(codec)}_{preset}" +def _stage_pyramids( + *, + read_path: Path, + staging_path: Path, + image_keys: list[str], + levels: int | Literal["auto"], + downscale: int, + min_size: int, + force: bool, +) -> tuple[list[dict[str, Any]], dict[str, Path]]: + """Build pyramids for *image_keys* into a staging store. + + Returns the per-raster reports and, for each key that was actually rebuilt, + the root to read its levels from. + """ + import spatialdata as sd + + from .pyramids import build_pyramids_into + + shutil.copytree(read_path, staging_path) + reports = build_pyramids_into( + source_sdata=sd.read_zarr(read_path), + dest_sdata=sd.read_zarr(staging_path), + keys=image_keys, + collection="images", + levels=levels, + downscale=downscale, + min_size=min_size, + force=force, + ) + roots = { + report["path"].split("/", 1)[1]: staging_path + for report in reports + if report.get("action") == "rebuilt" + } + return reports, roots + + def recompress_spatialdata( source: str | Path | Any, dest: str | Path, @@ -556,6 +595,11 @@ def recompress_spatialdata( reversible: bool | None = None, sibling: bool = False, workers: int | None = None, + pyramid: bool = False, + pyramid_levels: int | Literal["auto"] = "auto", + pyramid_downscale: int = 2, + pyramid_min_size: int = DEFAULT_PYRAMID_MIN_SIZE, + pyramid_force: bool = False, ) -> RecompressedSpatialData: """Preserve a SpatialData store and recompress configured rasters. @@ -570,6 +614,12 @@ def recompress_spatialdata( Path sources are copied before raster replacement, which keeps tables, shapes, points, and unconfigured rasters intact without loading the whole object. + + With *pyramid*, any configured image that has only one resolution level is + given a multiscale pyramid before it is recompressed, so single-resolution + acquisition output becomes browser-ready in one pass. Images that already + have a pyramid are left alone unless *pyramid_force* is set. See + `spatialdata_js_util.pyramids` for how levels are chosen. """ import os @@ -583,6 +633,7 @@ def recompress_spatialdata( dest_path = Path(dest) temp_dir: tempfile.TemporaryDirectory[str] | None = None + pyramid_dir: tempfile.TemporaryDirectory[str] | None = None try: if isinstance(source, str | Path): read_path = _prepare_path_source(Path(source), dest_path, overwrite=overwrite) @@ -616,11 +667,31 @@ def recompress_spatialdata( if not label_keys and resolved_config.get("default_labels", {}).get("codec") == "blosc": label_keys = _list_raster_keys(dest_path, "labels") + # Pyramids are staged in their own store rather than written straight into + # dest: the recompressor deletes a destination array before streaming the + # source into it, so reading the new levels from dest would pull the rug + # out from under itself. + pyramid_reports: list[dict[str, Any]] = [] + pyramid_roots: dict[str, Path] = {} + if pyramid: + pyramid_dir = tempfile.TemporaryDirectory(prefix="spatialdata-js-util-pyramid-") + pyramid_reports, pyramid_roots = _stage_pyramids( + read_path=read_path, + staging_path=Path(pyramid_dir.name) / "pyramid.zarr", + image_keys=image_keys, + levels=pyramid_levels, + downscale=pyramid_downscale, + min_size=pyramid_min_size, + force=pyramid_force, + ) + image_reports = [] default_image = resolved_config.get("default_image", {}) for key in image_keys: image_config = _deep_merge(default_image, resolved_config.get("images", {}).get(key, {})) resolved_codec = _resolve_image_codec(image_config, key) + # Rebuilt images are read from staging; everything else from the source. + raster_root = pyramid_roots.get(key, read_path) if sibling: dest_key = _sibling_image_key( @@ -632,20 +703,33 @@ def recompress_spatialdata( if sib_group_path.exists(): shutil.rmtree(sib_group_path) sib_group_path.mkdir(parents=True) - src_group_meta = read_path / "images" / key / "zarr.json" + src_group_meta = raster_root / "images" / key / "zarr.json" if src_group_meta.exists(): shutil.copy2(src_group_meta, sib_group_path / "zarr.json") else: dest_key = key + if key in pyramid_roots: + # Replace the copied single-level group wholesale so the store + # declares the new levels and keeps no stale ones. + dest_group = dest_path / "images" / dest_key + if dest_group.exists(): + shutil.rmtree(dest_group) + dest_group.mkdir(parents=True) + shutil.copy2( + raster_root / "images" / key / "zarr.json", dest_group / "zarr.json" + ) - for dataset in _datasets_from_raster_group( - dest_path / "images" / key if not sibling else read_path / "images" / key - ): + group_for_datasets = ( + raster_root / "images" / key + if sibling or key in pyramid_roots + else dest_path / "images" / key + ) + for dataset in _datasets_from_raster_group(group_for_datasets): source_raster = f"images/{key}/{dataset}" dest_raster = f"images/{dest_key}/{dataset}" image_reports.append( _recompress_image_array( - source_array_path=read_path / source_raster, + source_array_path=raster_root / source_raster, dest_array_path=dest_path / dest_raster, raster_path=dest_raster, config=image_config, @@ -684,6 +768,7 @@ def recompress_spatialdata( "images": image_reports, "labels": label_reports, "workers": worker_count, + "pyramids": pyramid_reports, "htj2k": backend_report(), "packages": { "imagecodecs": package_version("imagecodecs"), @@ -700,3 +785,5 @@ def recompress_spatialdata( finally: if temp_dir is not None: temp_dir.cleanup() + if pyramid_dir is not None: + pyramid_dir.cleanup() diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/pyramids.py b/python/spatialdata-js-util/src/spatialdata_js_util/pyramids.py new file mode 100644 index 00000000..2926214e --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/pyramids.py @@ -0,0 +1,312 @@ +"""Add multiscale pyramids to raster elements that lack them. + +Acquisition output is often written at a single resolution. A browser then has no +choice but to fetch full-resolution chunks no matter how far out the user is +zoomed, which is exactly the cost this package exists to avoid — so a store +without a pyramid defeats the image codecs rather than complementing them. + +Levels are generated through SpatialData's own parsers rather than by writing +`multiscales` metadata by hand. Each level carries a scale *and* a half-pixel +translation (`s1` is offset by 0.5, `s2` by 1.5, …); getting those wrong +misaligns every level against the full-resolution image, and the parsers already +know the rule. Images are coarsened by averaging, labels by the label model's own +downsampler, so label ids are never blended into ones that never existed. + +Rebuilding is always copy-then-write: the element is read from the source store +and written into a *different* store. SpatialData refuses to delete the files +backing a live element, so genuine in-place rewriting is not available here. +""" + +from __future__ import annotations + +import shutil +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal, Sequence + +from .errors import WriterCommandError +from .provenance import package_version +from .store import read_json, refresh_consolidated_metadata, write_json + +if TYPE_CHECKING: + from spatialdata import SpatialData + +RasterCollection = Literal["images", "labels"] + +#: Stop halving once the largest spatial axis is no bigger than this. Matches the +#: default spatial chunk size, so the coarsest level is about one chunk. +DEFAULT_MIN_SIZE = 1024 + +#: Guard against pathological inputs generating an unbounded number of levels. +MAX_LEVELS = 12 + + +@dataclass(frozen=True) +class PyramidResult: + store_path: Path + manifest_path: Path | None + manifest: dict[str, Any] + + +def _multiscale_datasets(store_path: Path, collection: str, key: str) -> list[str]: + """Return the dataset paths (`s0`, `s1`, …) declared by a raster group.""" + meta_path = store_path / collection / key / "zarr.json" + if not meta_path.is_file(): + return [] + attrs = read_json(meta_path).get("attributes", {}) + ome = attrs.get("ome", attrs) + if not isinstance(ome, dict): + return [] + multiscales = ome.get("multiscales") + if not multiscales: + return [] + return [str(d["path"]) for d in multiscales[0].get("datasets", []) if "path" in d] + + +def has_pyramid(store_path: str | Path, collection: str, key: str) -> bool: + """Whether a raster element already declares more than one resolution level.""" + return len(_multiscale_datasets(Path(store_path), collection, key)) > 1 + + +def _spatial_extent(element: Any) -> tuple[int, ...]: + """Sizes of the spatial axes (y/x, plus z when present).""" + sizes = element.sizes + return tuple(int(sizes[axis]) for axis in ("z", "y", "x") if axis in sizes) + + +def resolve_scale_factors( + element: Any, + *, + levels: int | Literal["auto"] = "auto", + downscale: int = 2, + min_size: int = DEFAULT_MIN_SIZE, +) -> list[int]: + """Return the per-step downscale factors to hand to a SpatialData parser. + + `levels` counts *total* resolutions including full res, so `levels=3` yields + two factors and the levels `s0`, `s1`, `s2`. With ``"auto"``, halving + continues until the largest spatial axis fits within *min_size*. + """ + if downscale < 2: + raise WriterCommandError(f"Pyramid downscale must be at least 2, got {downscale}.") + + extent = _spatial_extent(element) + if not extent: + return [] + + if levels != "auto": + if levels < 1: + raise WriterCommandError(f"Pyramid levels must be at least 1, got {levels}.") + return [downscale] * (min(int(levels), MAX_LEVELS) - 1) + + if min_size < 1: + raise WriterCommandError(f"Pyramid min-size must be positive, got {min_size}.") + + factors: list[int] = [] + largest = max(extent) + while largest > min_size and len(factors) < MAX_LEVELS - 1: + factors.append(downscale) + largest //= downscale + # A further level would shrink an axis below one pixel. + if min(extent) // (downscale ** len(factors)) < 1: + break + return factors + + +def _full_resolution(element: Any) -> Any: + """Return the full-resolution array, unwrapping an existing multiscale tree.""" + from xarray import DataTree + + if isinstance(element, DataTree): + first = next(iter(element)) + return element[first]["image"] + return element + + +def _rebuild_element(source_element: Any, scale_factors: list[int], chunks: Any) -> Any: + from spatialdata.models import get_model + + model = get_model(source_element) + # Transformations travel with the element; passing them again is an error. + return model.parse(source_element, scale_factors=scale_factors, chunks=chunks) + + +def _element_chunks(element: Any) -> tuple[int, ...] | None: + data = getattr(element, "data", None) + chunksize = getattr(data, "chunksize", None) + return tuple(int(size) for size in chunksize) if chunksize else None + + +def build_pyramids_into( + *, + source_sdata: "SpatialData", + dest_sdata: "SpatialData", + keys: Sequence[str], + collection: RasterCollection, + levels: int | Literal["auto"] = "auto", + downscale: int = 2, + min_size: int = DEFAULT_MIN_SIZE, + force: bool = False, +) -> list[dict[str, Any]]: + """Rebuild *keys* with pyramids, reading from one store and writing to another. + + `dest_sdata` must be backed by a different path than `source_sdata`; see the + module docstring. Returns one report per key, including skipped ones. + """ + reports: list[dict[str, Any]] = [] + dest_elements = getattr(dest_sdata, collection) + source_elements = getattr(source_sdata, collection) + + for key in keys: + source_element = source_elements[key] + existing = _multiscale_datasets(Path(str(dest_sdata.path)), collection, key) + if len(existing) > 1 and not force: + reports.append( + { + "path": f"{collection}/{key}", + "action": "skipped", + "reason": "already multiscale", + "levels": existing, + } + ) + continue + + full_res = _full_resolution(source_element) + scale_factors = resolve_scale_factors( + full_res, levels=levels, downscale=downscale, min_size=min_size + ) + if not scale_factors: + reports.append( + { + "path": f"{collection}/{key}", + "action": "skipped", + "reason": ( + "already at or below the target size" + if levels == "auto" + else "requested a single level" + ), + "shape": list(full_res.shape), + } + ) + continue + + rebuilt = _rebuild_element(full_res, scale_factors, _element_chunks(full_res)) + dest_elements[key] = rebuilt + dest_sdata.delete_element_from_disk(key) + dest_sdata.write_element(key) + + reports.append( + { + "path": f"{collection}/{key}", + "action": "rebuilt", + "shape": list(full_res.shape), + "scale_factors": scale_factors, + "levels": _multiscale_datasets(Path(str(dest_sdata.path)), collection, key), + } + ) + return reports + + +def _select_keys( + store_path: Path, collection: RasterCollection, requested: Sequence[str] | None +) -> list[str]: + from .store import list_element_keys + + available = list_element_keys(store_path, collection) + if requested is None: + return available + unknown = [key for key in requested if key not in available] + if unknown: + raise WriterCommandError( + f"Unknown {collection} element(s): {', '.join(unknown)}. " + f"Available: {', '.join(available) or '(none)'}" + ) + return list(requested) + + +def add_pyramids( + source: str | Path, + dest: str | Path, + *, + image_keys: Sequence[str] | None = None, + label_keys: Sequence[str] | None = None, + include_labels: bool = False, + levels: int | Literal["auto"] = "auto", + downscale: int = 2, + min_size: int = DEFAULT_MIN_SIZE, + force: bool = False, + overwrite: bool = False, + manifest: bool = True, +) -> PyramidResult: + """Copy a store and give its raster elements multiscale pyramids. + + Elements that already have more than one level are left alone unless *force* + is set. Everything the store contains that is not being rebuilt — points, + shapes, tables, other rasters — is preserved by the copy. + """ + import spatialdata as sd + + source_path = Path(source) + dest_path = Path(dest) + if not (source_path / "zarr.json").is_file(): + raise WriterCommandError(f"Not a zarr store (no zarr.json): {source_path}") + if dest_path.resolve() == source_path.resolve(): + raise WriterCommandError( + "Pyramids must be written to a different store than the source: SpatialData " + "refuses to delete the files backing a live element." + ) + if dest_path.exists(): + if not overwrite: + raise WriterCommandError( + f"Destination already exists: {dest_path}\nPass overwrite to replace it." + ) + shutil.rmtree(dest_path) + shutil.copytree(source_path, dest_path) + + source_sdata = sd.read_zarr(source_path) + dest_sdata = sd.read_zarr(dest_path) + + reports = build_pyramids_into( + source_sdata=source_sdata, + dest_sdata=dest_sdata, + keys=_select_keys(dest_path, "images", image_keys), + collection="images", + levels=levels, + downscale=downscale, + min_size=min_size, + force=force, + ) + if include_labels: + reports.extend( + build_pyramids_into( + source_sdata=source_sdata, + dest_sdata=dest_sdata, + keys=_select_keys(dest_path, "labels", label_keys), + collection="labels", + levels=levels, + downscale=downscale, + min_size=min_size, + force=force, + ) + ) + + refresh_consolidated_metadata(dest_path) + + manifest_data = { + "format": "spatialdata-pyramids/v1", + "source": str(source_path), + "output": str(dest_path), + "levels": levels, + "downscale": downscale, + "min_size": min_size, + "rasters": reports, + "packages": { + "spatialdata": package_version("spatialdata"), + "spatialdata-js-util": package_version("spatialdata-js-util"), + "zarr": package_version("zarr"), + }, + } + manifest_path = dest_path.with_suffix(".pyramid-manifest.json") if manifest else None + if manifest_path is not None: + write_json(manifest_path, manifest_data) + return PyramidResult(dest_path, manifest_path, manifest_data) diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py b/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py index d24085ad..f546dc36 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py @@ -28,6 +28,7 @@ ) from ..codecs import CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K +from ..pyramids import DEFAULT_MIN_SIZE as DEFAULT_PYRAMID_MIN_SIZE from ..errors import WriterCommandError from ..index_permutations import DEFAULT_CONDITIONS from ..runners import ( @@ -769,6 +770,18 @@ def compose(self) -> ComposeResult: yield Input(value=str(os.cpu_count() or 1), id="workers") yield Checkbox("Write siblings instead of replacing images", id="sibling") yield Checkbox("Overwrite destination if it exists", id="overwrite") + yield Static("Multiscale pyramid", classes="section-label") + yield Checkbox( + "Build a pyramid for images that have only one resolution level", + id="pyramid", + ) + yield Label("Levels ('auto' halves until the largest axis fits the size below)") + yield Input(value="auto", id="pyramid-levels") + yield Label("Downscale between levels") + yield Input(value="2", id="pyramid-downscale") + yield Label("Auto stops at this size (px)") + yield Input(value=str(DEFAULT_PYRAMID_MIN_SIZE), id="pyramid-min-size") + yield Checkbox("Rebuild images that already have a pyramid", id="pyramid-force") with Horizontal(): yield Button("Run", variant="primary", id="run") yield Button("Back", id="back") @@ -822,9 +835,32 @@ def _submit(self) -> None: self.notify(str(exc), severity="error") return + levels_raw = self.query_one("#pyramid-levels", Input).value.strip() or "auto" + if levels_raw == "auto": + pyramid_levels: Any = "auto" + else: + try: + pyramid_levels = _positive_int(levels_raw, 1) + except ValueError: + self.notify("Pyramid levels must be 'auto' or a positive integer.", severity="error") + return + try: + pyramid_downscale = _positive_int(self.query_one("#pyramid-downscale", Input).value, 2) + pyramid_min_size = _positive_int( + self.query_one("#pyramid-min-size", Input).value, DEFAULT_PYRAMID_MIN_SIZE + ) + except ValueError as exc: + self.notify(str(exc), severity="error") + return + if pyramid_downscale < 2: + self.notify("Pyramid downscale must be at least 2.", severity="error") + return + image_key = self.query_one("#image-key", Input).value.strip() or None sibling = self.query_one("#sibling", Checkbox).value overwrite = self.query_one("#overwrite", Checkbox).value + pyramid = self.query_one("#pyramid", Checkbox).value + pyramid_force = self.query_one("#pyramid-force", Checkbox).value self.app.context.zarr_path = source def runner() -> dict[str, Any]: @@ -842,6 +878,11 @@ def runner() -> dict[str, Any]: sibling=sibling, overwrite=overwrite, workers=workers, + pyramid=pyramid or pyramid_force, + pyramid_levels=pyramid_levels, + pyramid_downscale=pyramid_downscale, + pyramid_min_size=pyramid_min_size, + pyramid_force=pyramid_force, ).manifest dest_exists = Path(dest).exists() @@ -1132,6 +1173,12 @@ def _summary_lines(self, result: dict[str, Any]) -> ComposeResult: backend = (result.get("htj2k") or {}).get("selected") if backend: yield Static(f"HTJ2K backend: {backend}") + for entry in result.get("pyramids") or []: + if entry.get("action") == "rebuilt": + built = ", ".join(entry.get("levels") or []) + yield Static(f" pyramid {entry['path']}: {built}") + else: + yield Static(f" pyramid {entry['path']}: skipped ({entry.get('reason')})") elif command == CommandId.TABLES_TO_CSC: yield Static(f"Output: {result.get('output')}") for table in result.get("tables") or []: diff --git a/python/spatialdata-js-util/tests/test_pyramids.py b/python/spatialdata-js-util/tests/test_pyramids.py new file mode 100644 index 00000000..54c44dfb --- /dev/null +++ b/python/spatialdata-js-util/tests/test_pyramids.py @@ -0,0 +1,265 @@ +"""Multiscale pyramid generation for single-resolution rasters.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import numpy as np +import pytest + +from spatialdata_js_util.errors import WriterCommandError + +sd = pytest.importorskip("spatialdata") + +from spatialdata_js_util.pyramids import ( # noqa: E402 + MAX_LEVELS, + add_pyramids, + has_pyramid, + resolve_scale_factors, +) + + +def _image(height: int = 2048, width: int = 1536) -> np.ndarray: + y, x = np.mgrid[0:height, 0:width] + return np.stack([((x * 3 + y * 7) % 4096).astype(np.uint16)]) + + +def _labels(height: int = 2048, width: int = 1536) -> np.ndarray: + y, x = np.mgrid[0:height, 0:width] + return ((((x // 256) + (y // 256)) % 7) * 13 + 1).astype(np.uint32) + + +def _store( + path: Path, *, pixels: np.ndarray | None = None, labels: np.ndarray | None = None, **kwargs +) -> Path: + from spatialdata.models import Image2DModel, Labels2DModel + + images = {"morphology": Image2DModel.parse( + _image() if pixels is None else pixels, dims=("c", "y", "x"), **kwargs + )} + label_elements = ( + {"cells": Labels2DModel.parse(labels, dims=("y", "x"), **kwargs)} + if labels is not None + else {} + ) + sd.SpatialData(images=images, labels=label_elements).write(path, overwrite=True) + return path + + +def _levels(store: Path, collection: str, key: str) -> list[str]: + attrs = json.loads((store / collection / key / "zarr.json").read_text())["attributes"] + ome = attrs.get("ome", attrs) + return [d["path"] for d in ome["multiscales"][0]["datasets"]] + + +class TestResolveScaleFactors: + class _Element: + def __init__(self, **sizes: int) -> None: + self.sizes = sizes + + def test_auto_halves_until_within_min_size(self) -> None: + element = self._Element(c=1, y=4096, x=4096) + assert resolve_scale_factors(element, min_size=1024) == [2, 2] + + def test_auto_returns_nothing_when_already_small(self) -> None: + element = self._Element(c=1, y=512, x=512) + assert resolve_scale_factors(element, min_size=1024) == [] + + def test_auto_uses_the_largest_spatial_axis(self) -> None: + element = self._Element(c=1, y=256, x=4096) + assert resolve_scale_factors(element, min_size=1024) == [2, 2] + + def test_auto_accounts_for_z(self) -> None: + element = self._Element(c=1, z=8, y=2048, x=2048) + assert resolve_scale_factors(element, min_size=1024) == [2] + + def test_explicit_levels_counts_full_resolution(self) -> None: + element = self._Element(c=1, y=4096, x=4096) + assert resolve_scale_factors(element, levels=3) == [2, 2] + assert resolve_scale_factors(element, levels=1) == [] + + def test_downscale_is_respected(self) -> None: + element = self._Element(c=1, y=4096, x=4096) + assert resolve_scale_factors(element, downscale=4, min_size=1024) == [4] + + def test_auto_is_bounded(self) -> None: + element = self._Element(c=1, y=2**24, x=2**24) + assert len(resolve_scale_factors(element, min_size=1)) <= MAX_LEVELS - 1 + + def test_rejects_a_degenerate_downscale(self) -> None: + element = self._Element(c=1, y=4096, x=4096) + with pytest.raises(WriterCommandError, match="at least 2"): + resolve_scale_factors(element, downscale=1) + + def test_rejects_zero_levels(self) -> None: + element = self._Element(c=1, y=4096, x=4096) + with pytest.raises(WriterCommandError, match="at least 1"): + resolve_scale_factors(element, levels=0) + + +class TestAddPyramids: + def test_single_level_store_gains_levels(self, tmp_path: Path) -> None: + source = _store(tmp_path / "src.zarr") + assert not has_pyramid(source, "images", "morphology") + + result = add_pyramids(source, tmp_path / "out.zarr") + + dest = result.store_path + assert has_pyramid(dest, "images", "morphology") + assert _levels(dest, "images", "morphology") == ["s0", "s1"] + + def test_full_resolution_is_untouched(self, tmp_path: Path) -> None: + pixels = _image() + source = _store(tmp_path / "src.zarr", pixels=pixels) + + add_pyramids(source, tmp_path / "out.zarr") + + image = sd.read_zarr(tmp_path / "out.zarr").images["morphology"] + assert np.array_equal(np.asarray(image["scale0"]["image"].data.compute()), pixels) + + def test_downsampled_level_is_an_average(self, tmp_path: Path) -> None: + pixels = _image(1024, 1024) + source = _store(tmp_path / "src.zarr", pixels=pixels) + + add_pyramids(source, tmp_path / "out.zarr", levels=2) + + image = sd.read_zarr(tmp_path / "out.zarr").images["morphology"] + s1 = np.asarray(image["scale1"]["image"].data.compute()) + expected = pixels.reshape(1, 512, 2, 512, 2).mean(axis=(2, 4)).astype(np.uint16) + assert np.array_equal(s1, expected) + + def test_transformations_survive(self, tmp_path: Path) -> None: + from spatialdata.transformations import Scale, get_transformation + + source = _store(tmp_path / "src.zarr", transformations={"global": Scale([2.0, 2.0], axes=("y", "x"))}) + before = get_transformation(sd.read_zarr(source).images["morphology"], get_all=True) + + add_pyramids(source, tmp_path / "out.zarr") + + after = get_transformation(sd.read_zarr(tmp_path / "out.zarr").images["morphology"], get_all=True) + assert after == before + + def test_already_multiscale_is_skipped(self, tmp_path: Path) -> None: + source = _store(tmp_path / "src.zarr", scale_factors=[2]) + assert has_pyramid(source, "images", "morphology") + + result = add_pyramids(source, tmp_path / "out.zarr") + + entry = result.manifest["rasters"][0] + assert entry["action"] == "skipped" + assert entry["reason"] == "already multiscale" + + def test_force_rebuilds_an_existing_pyramid(self, tmp_path: Path) -> None: + source = _store(tmp_path / "src.zarr", scale_factors=[2]) + + result = add_pyramids(source, tmp_path / "out.zarr", levels=3, force=True) + + assert result.manifest["rasters"][0]["action"] == "rebuilt" + assert _levels(tmp_path / "out.zarr", "images", "morphology") == ["s0", "s1", "s2"] + + def test_small_image_is_skipped_not_rebuilt(self, tmp_path: Path) -> None: + source = _store(tmp_path / "src.zarr", pixels=_image(256, 256)) + + result = add_pyramids(source, tmp_path / "out.zarr") + + assert result.manifest["rasters"][0]["action"] == "skipped" + assert not has_pyramid(tmp_path / "out.zarr", "images", "morphology") + + def test_labels_are_left_alone_by_default(self, tmp_path: Path) -> None: + source = _store(tmp_path / "src.zarr", labels=_labels()) + + add_pyramids(source, tmp_path / "out.zarr") + + assert not has_pyramid(tmp_path / "out.zarr", "labels", "cells") + + def test_labels_downsample_without_inventing_ids(self, tmp_path: Path) -> None: + labels = _labels() + source = _store(tmp_path / "src.zarr", labels=labels) + + add_pyramids(source, tmp_path / "out.zarr", include_labels=True) + + element = sd.read_zarr(tmp_path / "out.zarr").labels["cells"] + original = set(np.unique(labels).tolist()) + coarse = set(np.unique(np.asarray(element["scale1"]["image"].data.compute())).tolist()) + # Averaging label ids would produce values that identify no object. + assert coarse <= original + + def test_other_elements_are_preserved(self, tmp_path: Path) -> None: + source = _store(tmp_path / "src.zarr", labels=_labels()) + + add_pyramids(source, tmp_path / "out.zarr") + + assert (tmp_path / "out.zarr" / "labels" / "cells").is_dir() + + def test_manifest_is_written(self, tmp_path: Path) -> None: + source = _store(tmp_path / "src.zarr") + result = add_pyramids(source, tmp_path / "out.zarr") + assert result.manifest_path is not None and result.manifest_path.is_file() + assert result.manifest["format"] == "spatialdata-pyramids/v1" + + def test_refuses_to_write_over_the_source(self, tmp_path: Path) -> None: + source = _store(tmp_path / "src.zarr") + with pytest.raises(WriterCommandError, match="different store"): + add_pyramids(source, source, overwrite=True) + + def test_rejects_existing_dest_without_overwrite(self, tmp_path: Path) -> None: + source = _store(tmp_path / "src.zarr") + (tmp_path / "out.zarr").mkdir() + with pytest.raises(WriterCommandError, match="already exists"): + add_pyramids(source, tmp_path / "out.zarr") + + def test_rejects_unknown_image_key(self, tmp_path: Path) -> None: + source = _store(tmp_path / "src.zarr") + with pytest.raises(WriterCommandError, match="Unknown images element"): + add_pyramids(source, tmp_path / "out.zarr", image_keys=["nope"]) + + def test_rejects_a_non_store_path(self, tmp_path: Path) -> None: + with pytest.raises(WriterCommandError, match="Not a zarr store"): + add_pyramids(tmp_path, tmp_path / "out.zarr") + + +class TestRecompressWithPyramid: + """The one-pass path: single-resolution input to browser-ready output.""" + + def test_builds_levels_and_encodes_every_one(self, tmp_path: Path) -> None: + from spatialdata_js_util import recompress_spatialdata + from spatialdata_js_util.codecs import CODEC_HTJ2K_OPENJPH, htj2k_available + + if not htj2k_available(): + pytest.skip("No HTJ2K backend available.") + + pixels = _image() + source = _store(tmp_path / "src.zarr", pixels=pixels) + dest = tmp_path / "out.zarr" + + result = recompress_spatialdata( + source, + dest, + codec=CODEC_HTJ2K_OPENJPH, + preset="lossless", + chunks="auto", + pyramid=True, + overwrite=True, + ) + + assert [r["path"] for r in result.manifest["images"]] == [ + "images/morphology/s0", + "images/morphology/s1", + ] + assert result.manifest["pyramids"][0]["action"] == "rebuilt" + + # Both levels must be readable, and full res still exact. + image = sd.read_zarr(dest).images["morphology"] + assert np.array_equal(np.asarray(image["scale0"]["image"].data.compute()), pixels) + assert tuple(image["scale1"]["image"].shape) == (1, 1024, 768) + + def test_without_the_flag_nothing_is_rebuilt(self, tmp_path: Path) -> None: + from spatialdata_js_util import recompress_spatialdata + + source = _store(tmp_path / "src.zarr") + result = recompress_spatialdata( + source, tmp_path / "out.zarr", preset="lossless", chunks="auto", overwrite=True + ) + assert result.manifest["pyramids"] == [] + assert not has_pyramid(tmp_path / "out.zarr", "images", "morphology") diff --git a/python/spatialdata-js-util/tests/test_tui.py b/python/spatialdata-js-util/tests/test_tui.py index 13ef5cd5..9d6a0e2a 100644 --- a/python/spatialdata-js-util/tests/test_tui.py +++ b/python/spatialdata-js-util/tests/test_tui.py @@ -45,11 +45,12 @@ def _store_with_table(path: Path) -> Path: return path -def _store_with_image(path: Path) -> np.ndarray: +def _store_with_image(path: Path, *, pixels: np.ndarray | None = None) -> np.ndarray: from spatialdata.models import Image2DModel - y, x = np.mgrid[0:64, 0:48] - pixels = np.stack([((x * 9 + y * 5) % 4096).astype(np.uint16)]) + if pixels is None: + y, x = np.mgrid[0:64, 0:48] + pixels = np.stack([((x * 9 + y * 5) % 4096).astype(np.uint16)]) sd.SpatialData( images={"morphology": Image2DModel.parse(pixels, dims=("c", "y", "x"))} ).write(path, overwrite=True) @@ -285,3 +286,72 @@ async def test_existing_destination_requires_the_overwrite_box(self, tmp_path: P app.screen.query_one("#run", Button).press() await pilot.pause() assert isinstance(app.screen, ConfirmScreen) + + +class TestRecompressPyramidControls: + @pytest.mark.asyncio + async def test_pyramid_checkbox_builds_levels(self, tmp_path: Path) -> None: + from spatialdata_js_util.pyramids import has_pyramid + + source = tmp_path / "src.zarr" + y, x = np.mgrid[0:2048, 0:1536] + pixels = np.stack([((x * 3 + y * 7) % 4096).astype(np.uint16)]) + _store_with_image(source, pixels=pixels) + dest = tmp_path / "out.zarr" + + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-recompress") + app.screen.query_one("#source", Input).value = str(source) + app.screen.query_one("#dest", Input).value = str(dest) + app.screen.query_one("#pyramid", Checkbox).value = True + await pilot.pause() + app.screen.query_one("#run", Button).press() + report = await _run_to_report(pilot) + assert report.error is None, report.error + assert report.result is not None + assert report.result["pyramids"][0]["action"] == "rebuilt" + + assert has_pyramid(dest, "images", "morphology") + + @pytest.mark.asyncio + async def test_pyramid_off_by_default(self, tmp_path: Path) -> None: + from spatialdata_js_util.pyramids import has_pyramid + + source = tmp_path / "src.zarr" + y, x = np.mgrid[0:2048, 0:1536] + _store_with_image(source, pixels=np.stack([((x + y) % 4096).astype(np.uint16)])) + dest = tmp_path / "out.zarr" + + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-recompress") + app.screen.query_one("#source", Input).value = str(source) + app.screen.query_one("#dest", Input).value = str(dest) + await pilot.pause() + app.screen.query_one("#run", Button).press() + report = await _run_to_report(pilot) + assert report.error is None, report.error + assert report.result["pyramids"] == [] + + assert not has_pyramid(dest, "images", "morphology") + + @pytest.mark.asyncio + async def test_degenerate_downscale_is_rejected(self, tmp_path: Path) -> None: + source = tmp_path / "src.zarr" + _store_with_image(source) + + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-recompress") + app.screen.query_one("#source", Input).value = str(source) + app.screen.query_one("#dest", Input).value = str(tmp_path / "out.zarr") + app.screen.query_one("#pyramid", Checkbox).value = True + app.screen.query_one("#pyramid-downscale", Input).value = "1" + await pilot.pause() + app.screen.query_one("#run", Button).press() + await pilot.pause() + assert isinstance(app.screen, RecompressImagesScreen) From e888e99ea7d062345fb53fb33c15912a04af07d6 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 28 Jul 2026 14:29:59 +0100 Subject: [PATCH 06/10] Fix consolidated metadata dropping implicit parent groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converting the tables of an index-permutations store to CSC left the store unopenable: `spatialdata.read_zarr` failed with GroupNotFoundError at the root, which looked like the table had lost its var names. The table was fine — var names, obs, and every X value were intact and correctly CSC. The fault was entirely in the root's consolidated metadata. `_collect_consolidated_metadata` rebuilds the listing by walking `zarr.json` files on disk. An index-permutations store has `points//zarr.json` for each element but no `points/zarr.json`, because `_copy_store_shell` skips the points directory and only per-element metadata is written afterwards. Rebuilding therefore dropped the `points` entry while keeping its four children, and zarr rejects the whole store when an entry's parent is missing — not just the affected element. It survived until now because the entry was inherited: the copied root zarr.json already listed `points`, and the index-permutations writer only appended sibling entries to it. Nothing rebuilt that listing from disk until the CSC conversion did. Two fixes, so neither half can reintroduce it: - `_collect_consolidated_metadata` fills in entries for intermediate groups that have no metadata file of their own, and `refresh_consolidated_metadata` now refuses to write a listing containing orphans rather than producing a store that cannot be opened. - `write_index_permutations` writes `points/zarr.json`, so the collection is a real group on disk instead of a bare directory. Co-Authored-By: Claude Opus 5 --- .../spatialdata_js_util/index_permutations.py | 22 +++++ .../src/spatialdata_js_util/store.py | 44 +++++++++- .../spatialdata-js-util/tests/test_tables.py | 81 +++++++++++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/index_permutations.py b/python/spatialdata-js-util/src/spatialdata_js_util/index_permutations.py index 65f0a2dd..9ba72824 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/index_permutations.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/index_permutations.py @@ -81,6 +81,27 @@ def _write_element_zarr_json(source_element_dir: Path, dest_element_dir: Path) - shutil.copy2(source_json, dest_element_dir / "zarr.json") +def _write_points_collection_zarr_json(source_path: Path, dest_path: Path) -> None: + """Recreate the `points/` group metadata that `_copy_store_shell` skipped. + + Without it the collection exists as a directory with no node metadata, so its + elements are listed in consolidated metadata under a parent that is not — an + orphan that stops the whole store from opening once anything rebuilds that + metadata from disk. + """ + dest_json = dest_path / "points" / "zarr.json" + if dest_json.is_file(): + return + dest_json.parent.mkdir(parents=True, exist_ok=True) + source_json = source_path / "points" / "zarr.json" + if source_json.is_file(): + shutil.copy2(source_json, dest_json) + return + dest_json.write_text( + json.dumps({"attributes": {}, "zarr_format": 3, "node_type": "group"}, indent=2) + "\n" + ) + + def _copy_canonical_parquet(source_parquet: Path, dest_parquet: Path) -> None: dest_parquet.parent.mkdir(parents=True, exist_ok=True) if source_parquet.is_dir(): @@ -123,6 +144,7 @@ def write_index_permutations( source_parquet = points_parquet_path(source_path, resolved_key) _copy_store_shell(source_path, dest_path, overwrite=overwrite) + _write_points_collection_zarr_json(source_path, dest_path) df = read_points_dataframe(source_parquet) if max_rows is not None and len(df) > max_rows: diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/store.py b/python/spatialdata-js-util/src/spatialdata_js_util/store.py index 9bc24b96..89c0bfeb 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/store.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/store.py @@ -31,16 +31,44 @@ def write_json(path: str | Path, value: dict[str, Any]) -> None: target.write_bytes(json_bytes(value)) -def _collect_consolidated_metadata(store_path: Path) -> dict[str, Any]: +def _implicit_group(zarr_format: int) -> dict[str, Any]: + return {"attributes": {}, "node_type": "group", "zarr_format": zarr_format} + + +def _collect_consolidated_metadata(store_path: Path, *, zarr_format: int = 3) -> dict[str, Any]: + """Map every node under *store_path* to its metadata document. + + Entries are filled in for intermediate groups that have no `zarr.json` of + their own. Some writers create element directories without writing metadata + for the collection that contains them — `points/transcripts/zarr.json` but no + `points/zarr.json`. Listing the child without its parent leaves an orphan + that zarr cannot attach to the hierarchy, and it rejects the *whole* store: + `zarr.open_group` then raises `GroupNotFoundError` at the root, so the store + stops opening entirely rather than merely missing one element. + """ metadata: dict[str, Any] = {} for meta_path in store_path.rglob("zarr.json"): if meta_path == store_path / "zarr.json": continue rel = meta_path.parent.relative_to(store_path).as_posix() metadata[rel] = read_json(meta_path) + + for rel in list(metadata): + parts = rel.split("/") + for depth in range(1, len(parts)): + parent = "/".join(parts[:depth]) + if parent not in metadata: + metadata[parent] = _implicit_group(zarr_format) return metadata +def consolidated_metadata_orphans(metadata: dict[str, Any]) -> list[str]: + """Entries whose parent path is absent — these make a store unreadable.""" + return sorted( + key for key in metadata if "/" in key and key.rsplit("/", 1)[0] not in metadata + ) + + def refresh_consolidated_metadata(store_path: str | Path) -> None: """Rewrite the store root's inline consolidated metadata from disk. @@ -50,10 +78,22 @@ def refresh_consolidated_metadata(store_path: str | Path) -> None: root = Path(store_path) root_path = root / "zarr.json" root_meta = read_json(root_path) + metadata = _collect_consolidated_metadata( + root, zarr_format=int(root_meta.get("zarr_format", 3)) + ) + + orphans = consolidated_metadata_orphans(metadata) + if orphans: + # Never write metadata that would make the store fail to open. + raise ValueError( + f"Refusing to write consolidated metadata with orphaned entries for {root}: " + f"{', '.join(orphans)}" + ) + root_meta["consolidated_metadata"] = { "kind": "inline", "must_understand": False, - "metadata": _collect_consolidated_metadata(root), + "metadata": metadata, } write_json(root_path, root_meta) diff --git a/python/spatialdata-js-util/tests/test_tables.py b/python/spatialdata-js-util/tests/test_tables.py index 013fec47..90e44f35 100644 --- a/python/spatialdata-js-util/tests/test_tables.py +++ b/python/spatialdata-js-util/tests/test_tables.py @@ -159,3 +159,84 @@ def test_selects_a_single_table(self, tmp_path: Path) -> None: reopened = sd.read_zarr(store) assert reopened.tables["first"].X.format == "csc" assert reopened.tables["second"].X.format == "csr" + + +class TestConsolidatedMetadataSurvivesConversion: + """Regression: a CSC conversion must not make the store unopenable. + + An index-permutations store has `points//` directories but historically + no `points/zarr.json`. Rebuilding consolidated metadata purely from on-disk + metadata files dropped the `points` parent, orphaning its children, and zarr + then refused to open the store *at all* — which looked like element data loss + rather than a metadata fault. + """ + + @staticmethod + def _store_with_points(path: Path) -> Path: + import pandas as pd + from spatialdata.models import PointsModel + + rng = np.random.default_rng(0) + points = PointsModel.parse( + pd.DataFrame( + { + "x": rng.uniform(0, 100, 500), + "y": rng.uniform(0, 100, 500), + "feature_name": pd.Categorical(rng.choice(["a", "b", "c"], 500)), + } + ), + feature_key="feature_name", + ) + sd.SpatialData(tables={"table": _table()}, points={"transcripts": points}).write( + path, overwrite=True + ) + return path + + def test_csc_conversion_keeps_an_index_permutations_store_readable( + self, tmp_path: Path + ) -> None: + from spatialdata_js_util.index_permutations import write_index_permutations + + source = self._store_with_points(tmp_path / "src.zarr") + permuted = tmp_path / "perm.zarr" + write_index_permutations(source, permuted, points_key="transcripts") + + # Readable before the conversion... + assert sd.read_zarr(permuted) is not None + + convert_store_tables_to_csc(permuted) + + # ...and still readable after it. + reopened = sd.read_zarr(permuted) + assert reopened.tables["table"].X.format == "csc" + assert "transcripts" in reopened.points + + def test_index_permutations_writes_the_points_group(self, tmp_path: Path) -> None: + from spatialdata_js_util.index_permutations import write_index_permutations + + source = self._store_with_points(tmp_path / "src.zarr") + permuted = tmp_path / "perm.zarr" + write_index_permutations(source, permuted, points_key="transcripts") + + assert (permuted / "points" / "zarr.json").is_file() + + def test_refresh_refuses_to_write_orphaned_entries(self, tmp_path: Path) -> None: + """The guard that would have turned this into a loud failure.""" + import json + + from spatialdata_js_util.store import ( + consolidated_metadata_orphans, + refresh_consolidated_metadata, + ) + + store = self._store_with_points(tmp_path / "src.zarr") + # Simulate the writer that produced the broken store. + (store / "points" / "zarr.json").unlink() + + refresh_consolidated_metadata(store) + + meta = json.loads((store / "zarr.json").read_text())["consolidated_metadata"]["metadata"] + assert consolidated_metadata_orphans(meta) == [] + # The missing parent is filled in rather than dropped. + assert meta["points"]["node_type"] == "group" + assert sd.read_zarr(store) is not None From 80bf5e1a8a29a151d58fa96535cf1409a356abc8 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Tue, 28 Jul 2026 14:44:49 +0100 Subject: [PATCH 07/10] Scope CSC conversion to the matrices it converts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converting a table written by an older stack left its variable names unreadable in the browser: the JS runtime fell back to `var0..varN`. Every value was present — the encoding had changed underneath it. The conversion re-serialised the whole AnnData, so AnnData 0.13 re-encoded parts of the table it had no reason to touch: - `var/_index` and `obs/_index` became `nullable-string-array` *groups* of `values`/`mask` rather than plain string arrays, so a reader looking for an array found a group and fell back to synthetic names; - every array gained the `sharding_indexed` codec; - `object` columns were normalised to `category`. Now only `X` and any named layers are rewritten, via `write_elem` on the table's zarr group, leaving `obs`/`var`/`uns` byte-identical. What is written pins `auto_shard_zarr_v3` and `allow_write_nullable_strings` off, so a rewritten matrix matches the encodings already in the store. Also drop the table group's own consolidated listing after converting. It caches the encoding of everything below it, and zarr trusts it over the files — X read back as a crc32c checksum failure while the bytes on disk were correct. Tests cover each half: non-matrix nodes are byte-identical across a conversion, a legacy plain-string index is not upgraded, the matrix is not sharded, and a table carrying a stale nested listing still reads. Co-Authored-By: Claude Opus 5 --- python/spatialdata-js-util/README.md | 7 + .../src/spatialdata_js_util/store.py | 22 +++ .../src/spatialdata_js_util/tables.py | 147 ++++++++------ .../spatialdata-js-util/tests/test_tables.py | 187 ++++++++++++++++++ 4 files changed, 303 insertions(+), 60 deletions(-) diff --git a/python/spatialdata-js-util/README.md b/python/spatialdata-js-util/README.md index c0688c09..9da3fb59 100644 --- a/python/spatialdata-js-util/README.md +++ b/python/spatialdata-js-util/README.md @@ -270,6 +270,13 @@ The conversion changes only the sparse layout, not the values, and the result stays readable by any AnnData client. Dense matrices are left alone unless you pass `--densify`, since sparsifying a dense matrix can make it bigger. +Only the matrices are rewritten — `obs`, `var`, `uns` and the rest are left +byte-for-byte alone, and what is written is pinned to the store's existing +encodings (no sharding, no `nullable-string-array` index). Re-serialising a whole +table would otherwise silently re-encode it with whatever conventions the +installed AnnData currently prefers, which can leave readers unable to find the +variable names even though every value is still present. + ```python from spatialdata_js_util import convert_store_tables_to_csc, to_csc diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/store.py b/python/spatialdata-js-util/src/spatialdata_js_util/store.py index 89c0bfeb..129908ec 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/store.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/store.py @@ -31,6 +31,28 @@ def write_json(path: str | Path, value: dict[str, Any]) -> None: target.write_bytes(json_bytes(value)) +def drop_consolidated_metadata(group_path: str | Path) -> bool: + """Remove a group's own inline consolidated metadata, if it has any. + + A nested block is a cache of the metadata below it. Rewriting anything under + the group leaves that cache describing the old encoding, and readers trust + the cache over the files — so an array can be rewritten correctly and still + fail to load. The store root's listing is refreshed separately and remains + the authoritative one. + + Returns whether a block was actually removed. + """ + meta_path = Path(group_path) / "zarr.json" + if not meta_path.is_file(): + return False + doc = read_json(meta_path) + if "consolidated_metadata" not in doc: + return False + doc.pop("consolidated_metadata") + write_json(meta_path, doc) + return True + + def _implicit_group(zarr_format: int) -> dict[str, Any]: return {"attributes": {}, "node_type": "group", "zarr_format": zarr_format} diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/tables.py b/python/spatialdata-js-util/src/spatialdata_js_util/tables.py index fabbc171..59a42ffb 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/tables.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/tables.py @@ -9,20 +9,28 @@ The conversion is lossless and reversible: only the sparse layout changes, not the values, and `scipy`/`anndata` read either layout transparently. It is therefore safe to apply to a store that other tools also read. + +Only the matrices themselves are rewritten. An earlier version re-serialised the +whole AnnData, which quietly re-encoded `obs` and `var` with the installed +AnnData's current conventions — turning the index into a `nullable-string-array` +group and sharding the arrays — so readers that had been fine with the store +suddenly could not find the variable names. Everything written here is therefore +scoped to the matrix being converted, and pinned to the older encodings. """ from __future__ import annotations import shutil +from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, Sequence +from typing import TYPE_CHECKING, Any, Iterator, Sequence from .errors import WriterCommandError from .provenance import package_version from .store import ( + drop_consolidated_metadata, list_element_keys, - read_json, refresh_consolidated_metadata, write_json, zarr_format_of, @@ -108,72 +116,93 @@ def _describe(before: str | None, after: str | None) -> str: return f"{before or 'dense'}->{after}" -def _read_table(path: Path) -> "AnnData": - import anndata as ad +#: AnnData settings pinned while writing, so a matrix we rewrite is encoded the +#: way the rest of the store already is. Names absent from the installed AnnData +#: are skipped. +#: +#: `auto_shard_zarr_v3` wraps arrays in `sharding_indexed`, and +#: `allow_write_nullable_strings` stores a string column as a +#: `nullable-string-array` *group* of `values`/`mask` instead of a plain string +#: array. Both are newer AnnData defaults that older readers — including the +#: JS runtime this package targets — do not understand. +_PINNED_WRITE_SETTINGS: dict[str, Any] = { + "auto_shard_zarr_v3": False, + "allow_write_nullable_strings": False, +} - return ad.read_zarr(path) +@contextmanager +def _anndata_write_settings(*, zarr_format: int) -> Iterator[None]: + import anndata as ad -#: Group attributes owned by AnnData's own encoding, which it rewrites itself. -_ANNDATA_OWNED_ATTRS = frozenset({"encoding-type", "encoding-version"}) + settings = ad.settings + pinned = {**_PINNED_WRITE_SETTINGS, "zarr_write_format": zarr_format} + previous = { + name: getattr(settings, name) for name in pinned if hasattr(settings, name) + } + try: + for name, value in pinned.items(): + if name in previous: + setattr(settings, name, value) + yield + finally: + for name, value in previous.items(): + setattr(settings, name, value) -def _group_attrs_path(path: Path) -> tuple[Path, str | None]: - """Return the metadata file holding a group's attributes, and its nesting key.""" - if (path / "zarr.json").is_file(): - return path / "zarr.json", "attributes" - if (path / ".zattrs").is_file(): - return path / ".zattrs", None - raise FileNotFoundError(f"No zarr group attributes found at {path}") +def _convert_group_matrix(group: Any, name: str, *, densify: bool) -> str: + """Convert one matrix child of a zarr group in place, reporting what changed.""" + from anndata.io import read_elem, write_elem + matrix = read_elem(group[name]) + before = _sparse_format(matrix) + converted = to_csc(matrix, densify=densify) + if converted is not matrix: + write_elem(group, name, converted) + return _describe(before, _sparse_format(converted)) -def _read_group_attrs(path: Path) -> dict[str, Any]: - meta_path, key = _group_attrs_path(path) - doc = read_json(meta_path) - return dict(doc.get(key, {})) if key else dict(doc) +def _convert_table_matrices( + table_path: Path, *, layers: bool, densify: bool +) -> dict[str, str]: + """Rewrite a table's matrices in place, touching nothing else. -def _merge_group_attrs(path: Path, attrs: dict[str, Any]) -> None: - meta_path, key = _group_attrs_path(path) - doc = read_json(meta_path) - if key: - merged = {**dict(doc.get(key, {})), **attrs} - doc[key] = merged - else: - doc = {**doc, **attrs} - write_json(meta_path, doc) + Only the matrix children are rewritten. Re-serialising the whole AnnData + would re-encode `obs`, `var`, `uns` and friends with whatever conventions the + installed AnnData currently prefers, silently changing parts of the store the + conversion has no business touching. + """ + import zarr + # Consolidated metadata makes the group read-only; the store root's copy is + # refreshed once at the end of the conversion instead. + root = zarr.open_group(str(table_path), mode="a", use_consolidated=False) + report: dict[str, str] = {} -def _write_table(adata: "AnnData", path: Path, *, zarr_format: int) -> None: - """Rewrite a table group, preserving SpatialData's element attributes. + with _anndata_write_settings(zarr_format=zarr_format_of(table_path)): + report["X"] = _convert_group_matrix(root, "X", densify=densify) + if layers and "layers" in root: + layers_group = root["layers"] + for name in sorted(layers_group.keys()): + report[f"layers/{name}"] = _convert_group_matrix( + layers_group, name, densify=densify + ) - `AnnData.write_zarr` writes only AnnData's own group attributes, so a plain - rewrite silently drops the `spatialdata-encoding-type`, `region`, - `region_key`, `instance_key`, and `version` keys that SpatialData puts on the - group — and `read_zarr` then fails on the missing version. We therefore - capture the group's attributes first and merge the non-AnnData ones back. - """ - import anndata as ad + # The table group may carry its own consolidated listing, which now describes + # the pre-conversion encoding. Readers trust it over the files, so a matrix + # rewritten correctly would still fail to load. + drop_consolidated_metadata(table_path) + return report - preserved = { - key: value - for key, value in _read_group_attrs(path).items() - if key not in _ANNDATA_OWNED_ATTRS - } - # AnnData writes whichever zarr format its global setting names; a store must - # not end up with mixed-format groups, so pin it to the format we read. - previous = ad.settings.zarr_write_format - ad.settings.zarr_write_format = zarr_format - try: - if path.exists(): - shutil.rmtree(path) - adata.write_zarr(path) - finally: - ad.settings.zarr_write_format = previous +def _table_dimensions(table_path: Path) -> tuple[int, int]: + import zarr - if preserved: - _merge_group_attrs(path, preserved) + root = zarr.open_group(str(table_path), mode="r", use_consolidated=False) + shape = root["X"].attrs.get("shape") + if shape is None: # dense X is a plain array + shape = root["X"].shape + return int(shape[0]), int(shape[1]) def convert_store_tables_to_csc( @@ -224,16 +253,14 @@ def convert_store_tables_to_csc( reports: list[dict[str, Any]] = [] for key in selected: table_path = store_path / "tables" / key - zarr_format = zarr_format_of(table_path) - adata = _read_table(table_path) - matrices = anndata_to_csc(adata, layers=layers, densify=densify) - _write_table(adata, table_path, zarr_format=zarr_format) + n_obs, n_vars = _table_dimensions(table_path) + matrices = _convert_table_matrices(table_path, layers=layers, densify=densify) reports.append( { "path": f"tables/{key}", - "n_obs": int(adata.n_obs), - "n_vars": int(adata.n_vars), - "zarr_format": zarr_format, + "n_obs": n_obs, + "n_vars": n_vars, + "zarr_format": zarr_format_of(table_path), "matrices": matrices, } ) diff --git a/python/spatialdata-js-util/tests/test_tables.py b/python/spatialdata-js-util/tests/test_tables.py index 90e44f35..6f0a7788 100644 --- a/python/spatialdata-js-util/tests/test_tables.py +++ b/python/spatialdata-js-util/tests/test_tables.py @@ -240,3 +240,190 @@ def test_refresh_refuses_to_write_orphaned_entries(self, tmp_path: Path) -> None # The missing parent is filled in rather than dropped. assert meta["points"]["node_type"] == "group" assert sd.read_zarr(store) is not None + + +class TestConversionDoesNotReEncodeTheRestOfTheTable: + """Regression: the conversion must touch the matrices and nothing else. + + Re-serialising the whole AnnData re-encoded `obs`/`var` with the installed + AnnData's current conventions — a `nullable-string-array` group instead of a + plain string array for the index, and `sharding_indexed` on every array. The + values were all still there, but readers looking for `var/_index` as an array + found a group and fell back to synthetic `var0..varN` names. + """ + + @staticmethod + def _node_encoding(path: Path) -> str: + doc = json.loads((path / "zarr.json").read_text()) + if doc.get("node_type") == "group": + return f"group[{doc.get('attributes', {}).get('encoding-type')}]" + return f"array[{doc.get('data_type')}] {[c['name'] for c in doc.get('codecs', [])]}" + + @staticmethod + def _named_table(n_obs: int = 40, n_vars: int = 6): + from spatialdata.models import TableModel + + matrix = sparse.random( + n_obs, n_vars, density=0.4, format="csr", random_state=0, dtype=np.float32 + ) + adata = anndata.AnnData(X=matrix) + adata.var.index = [f"GENE{i}" for i in range(n_vars)] + adata.obs.index = [f"cell{i}" for i in range(n_obs)] + adata.obs["region"] = "img" + adata.obs["region"] = adata.obs["region"].astype("category") + adata.obs["instance_id"] = np.arange(n_obs) + return TableModel.parse( + adata, region="img", region_key="region", instance_key="instance_id" + ) + + def _store(self, path: Path) -> Path: + sd.SpatialData(tables={"table": self._named_table()}).write(path, overwrite=True) + return path + + def test_var_and_obs_encoding_is_byte_identical(self, tmp_path: Path) -> None: + store = self._store(tmp_path / "s.zarr") + table = store / "tables" / "table" + before = { + str(p.parent.relative_to(table)): p.read_bytes() + for p in sorted(table.rglob("zarr.json")) + if p.parent.name != "X" and "X/" not in str(p.parent.relative_to(table)) + } + + convert_store_tables_to_csc(store) + + after = { + str(p.parent.relative_to(table)): p.read_bytes() + for p in sorted(table.rglob("zarr.json")) + if p.parent.name != "X" and "X/" not in str(p.parent.relative_to(table)) + } + changed = [k for k in before if before.get(k) != after.get(k)] + assert changed == [], f"conversion re-encoded non-matrix nodes: {changed}" + + def test_var_index_encoding_is_preserved(self, tmp_path: Path) -> None: + store = self._store(tmp_path / "s.zarr") + index_path = store / "tables" / "table" / "var" / "_index" + before = self._node_encoding(index_path) + + convert_store_tables_to_csc(store) + + assert self._node_encoding(index_path) == before + + def test_a_plain_string_index_is_not_upgraded(self, tmp_path: Path) -> None: + """The case that broke: an older store must not gain the newer encoding. + + Stores written by earlier stacks hold `var/_index` as a plain string + array. Newer AnnData writes a `nullable-string-array` group instead, and + a conversion that re-encodes the index silently upgrades the store out + from under readers that only understand the array form. + """ + import anndata as ad + + store = self._store(tmp_path / "s.zarr") + index_path = store / "tables" / "table" / "var" / "_index" + + # Rewrite the index the way an older stack would have. + if not hasattr(ad.settings, "allow_write_nullable_strings"): + pytest.skip("This AnnData has no nullable-string setting to pin.") + import zarr + from anndata.io import read_elem, write_elem + + root = zarr.open_group(str(store / "tables" / "table"), mode="a", use_consolidated=False) + var = read_elem(root["var"]) + previous = ad.settings.allow_write_nullable_strings + ad.settings.allow_write_nullable_strings = False + try: + write_elem(root, "var", var) + finally: + ad.settings.allow_write_nullable_strings = previous + + legacy = self._node_encoding(index_path) + assert legacy.startswith("array["), f"failed to build the legacy layout: {legacy}" + + convert_store_tables_to_csc(store) + + assert self._node_encoding(index_path) == legacy + assert list(sd.read_zarr(store).tables["table"].var.index) == [ + f"GENE{i}" for i in range(6) + ] + + def test_var_names_are_readable_after_conversion(self, tmp_path: Path) -> None: + store = self._store(tmp_path / "s.zarr") + convert_store_tables_to_csc(store) + table = sd.read_zarr(store).tables["table"] + assert list(table.var.index) == [f"GENE{i}" for i in range(6)] + assert list(table.obs.index) == [f"cell{i}" for i in range(40)] + + def test_matrix_is_not_sharded(self, tmp_path: Path) -> None: + """Sharded chunks are a newer zarr feature older readers cannot decode.""" + store = self._store(tmp_path / "s.zarr") + + convert_store_tables_to_csc(store) + + for part in ("data", "indices", "indptr"): + encoding = self._node_encoding(store / "tables" / "table" / "X" / part) + assert "sharding_indexed" not in encoding, f"X/{part} was sharded: {encoding}" + + def test_var_dtypes_are_not_normalised(self, tmp_path: Path) -> None: + """object->category drift was harmless but still an unasked-for change.""" + store = self._store(tmp_path / "s.zarr") + table_path = store / "tables" / "table" + import zarr + + root = zarr.open_group(str(table_path), mode="a", use_consolidated=False) + from anndata.io import write_elem + + import pandas as pd + + var = pd.DataFrame( + {"genome": pd.Series(["GRCh38"] * 6, dtype=object)}, + index=[f"GENE{i}" for i in range(6)], + ) + write_elem(root, "var", var) + before = json.loads((table_path / "var" / "genome" / "zarr.json").read_text()) + + convert_store_tables_to_csc(store) + + after = json.loads((table_path / "var" / "genome" / "zarr.json").read_text()) + assert after == before + + +class TestNestedConsolidatedMetadata: + """A table group's own consolidated listing must not survive a rewrite. + + It caches the encoding of everything below it. Left in place after the + matrices are rewritten it describes the old codecs, and zarr trusts it over + the actual files — the array reads back as a checksum failure even though + the bytes on disk are correct. + """ + + def test_stale_nested_listing_is_dropped(self, tmp_path: Path) -> None: + import zarr + + store = _store(tmp_path / "s.zarr") + table_path = store / "tables" / "table" + + # Give the table group a consolidated listing, as older writers did. + zarr.consolidate_metadata(zarr.open_group(str(table_path), mode="a").store) + assert "consolidated_metadata" in json.loads((table_path / "zarr.json").read_text()) + + convert_store_tables_to_csc(store) + + assert "consolidated_metadata" not in json.loads( + (table_path / "zarr.json").read_text() + ) + assert sd.read_zarr(store).tables["table"].X.format == "csc" + + def test_conversion_is_readable_with_a_pre_existing_nested_listing( + self, tmp_path: Path + ) -> None: + import zarr + + store = _store(tmp_path / "s.zarr") + table_path = store / "tables" / "table" + zarr.consolidate_metadata(zarr.open_group(str(table_path), mode="a").store) + + convert_store_tables_to_csc(store) + + table = sd.read_zarr(store).tables["table"] + assert table.X.format == "csc" + assert table.X.nnz > 0 From 77c8f552aae75640eb426549e06521be0ad9c589 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 31 Jul 2026 15:24:19 +0100 Subject: [PATCH 08/10] Address review findings on the Python package Data loss and wrong results first: - `convert_store_tables_to_csc` deleted the source when the destination resolved to it, or to a directory containing it: the overwrite branch rmtree's the destination before copying. Now refused before anything is removed, compared on resolved paths. - The TUI ran straight into that. An existing destination now needs the overwrite box (reported on the form rather than failing mid-run), and replacing one is confirmed the way an in-place rewrite already was. - `resolve_scale_factors` silently clamped an explicit `levels` to `MAX_LEVELS`, building a different pyramid from the one requested and reporting success. It now refuses. - `verify_index_permutations_manifest` reported `all_passed` for a Morton condition backed by a directory dataset, whose ordering it never checked. - `_feature_codes` returned `None` with no scenario selected while `_load_bounds` defaulted to the first scenario, benchmarking that scenario's bounds without its feature filter. Errors that arrived as tracebacks: - A table with no `X` raised `KeyError`. `X` is optional in AnnData, so it is now reported as absent and the layers still convert. - `_recompress_chunks` raised `ArgumentTypeError` from the command body, where argparse no longer catches it. - `verify_multiscale_parquet` propagated a parquet read error instead of reporting the unreadable file as the finding, unlike its Morton sibling. - `LegacyHtj2kCodec` inherits the encode path, so writing through it failed with a generic unsupported-codec error two layers down. The label is registered for reading existing stores only, and now says so. Tests that were not testing what they claimed: - A single HTJ2K gate gated a parametrised HTJ2K/JPEG2K pair, so JPEG2K skipped when only imagecodecs was present and failed when only the WASM backend was. Gated per codec instead. - `test_selected_backend_passes_the_probe` asserted nothing unless the selection happened to be imagecodecs. - `test_refresh_refuses_to_write_orphaned_entries` asserted successful reconstruction, leaving the refusal untested. Renamed, and the guard now has its own test. - Three TUI tests ran a real recompression with no backend guard. Also: docs said imagecodecs was off the HTJ2K path, which the probe-gated backend selection reversed. Co-Authored-By: Claude Opus 5 --- python/spatialdata-js-util/README.md | 2 +- .../docs/multi-component-codec-findings.md | 11 +++-- .../scripts/benchmark_points_index.py | 8 +++- .../src/spatialdata_js_util/cli.py | 5 +- .../spatialdata_js_util/codecs/zarr_codec.py | 13 ++++- .../src/spatialdata_js_util/pyramids.py | 8 +++- .../src/spatialdata_js_util/store.py | 13 +++-- .../src/spatialdata_js_util/tables.py | 25 +++++++++- .../src/spatialdata_js_util/tui/screens.py | 47 +++++++++++++++---- .../src/spatialdata_js_util/verify.py | 33 ++++++++++--- .../tests/test_pyramids.py | 7 +++ .../spatialdata-js-util/tests/test_tables.py | 46 +++++++++++++++++- python/spatialdata-js-util/tests/test_tui.py | 43 +++++++++++++++++ .../spatialdata-js-util/tests/test_verify.py | 7 +-- .../tests/test_zarr_codec.py | 38 +++++++++++++-- 15 files changed, 264 insertions(+), 42 deletions(-) diff --git a/python/spatialdata-js-util/README.md b/python/spatialdata-js-util/README.md index 9da3fb59..75fd8f16 100644 --- a/python/spatialdata-js-util/README.md +++ b/python/spatialdata-js-util/README.md @@ -55,7 +55,7 @@ spatialdata-js-util tui ~/data/xenium.zarr Pick a command from the home menu and the UI walks you through it: -``` +```text IMAGES Recompress rasters (JPEG 2000 / HTJ2K) POINTS List Points elements in a Zarr store POINTS Morton-sort Points from Zarr diff --git a/python/spatialdata-js-util/docs/multi-component-codec-findings.md b/python/spatialdata-js-util/docs/multi-component-codec-findings.md index 3931cd3b..2337fb9c 100644 --- a/python/spatialdata-js-util/docs/multi-component-codec-findings.md +++ b/python/spatialdata-js-util/docs/multi-component-codec-findings.md @@ -130,6 +130,11 @@ On the read side, `packages/zarrextra/src/codecs.ts` decodes the codestream to a planar component-major buffer and validates `decoded length === product(chunk_shape)`, so a multi-component chunk maps straight onto `[..., z, y, x]`. -`imagecodecs` is no longer on the HTJ2K path (encode and decode both go through -the openjph-wasm worker — the same WASM as the JS reader); it remains only for -the JPEG2000 fixture path. +`imagecodecs` is back on the HTJ2K path, and is now the preferred backend: its +openjph 0.30.1 decodes multi-component codestreams correctly and honours `qstep`, +so choosing it costs no quality control and removes Node.js from the install. +That preference is not taken on trust — a backend is only selected after it +decodes the committed multi-component probe fixture exactly (`codecs/backends.py`), +which is what the earlier WASM build failed. The openjph-wasm worker remains as +the fallback when `imagecodecs` is absent or fails the probe. `imagecodecs` also +still serves the JPEG2000 fixture path, which is unrelated to backend selection. diff --git a/python/spatialdata-js-util/scripts/benchmark_points_index.py b/python/spatialdata-js-util/scripts/benchmark_points_index.py index 57b08aaa..1b165efb 100644 --- a/python/spatialdata-js-util/scripts/benchmark_points_index.py +++ b/python/spatialdata-js-util/scripts/benchmark_points_index.py @@ -25,9 +25,15 @@ def _load_bounds(manifest: dict, scenario_id: str | None) -> dict[str, float]: def _feature_codes(manifest: dict, scenario_id: str | None) -> list[int] | None: + # Defaults to the same scenario `_load_bounds` picks. Diverging here would + # measure scenario 0's bounds without scenario 0's feature filter — a query + # no scenario describes, reported under that scenario's name. scenarios = manifest.get("benchmark_scenarios") or [] if not scenario_id: - return None + if not scenarios: + return None + codes = scenarios[0].get("feature_codes") + return list(codes) if codes is not None else None for scenario in scenarios: if scenario.get("id") == scenario_id: codes = scenario.get("feature_codes") diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/cli.py b/python/spatialdata-js-util/src/spatialdata_js_util/cli.py index 65c97ec4..dedb9409 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/cli.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/cli.py @@ -66,7 +66,10 @@ def _recompress_chunks(value: list[str] | None): try: return tuple(int(part) for part in value) except ValueError as exc: - raise argparse.ArgumentTypeError("chunks must be 'auto' or integer axis sizes") from exc + # Called from the command body rather than as an argparse `type=`, so an + # ArgumentTypeError here would surface as a traceback rather than a usage + # error. Match how the rest of the command reports bad arguments. + raise SystemExit("error: chunks must be 'auto' or integer axis sizes") from exc def _images_recompress(args: argparse.Namespace) -> None: diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/zarr_codec.py b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/zarr_codec.py index e8ebaa6e..00ad30d8 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/zarr_codec.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/zarr_codec.py @@ -104,10 +104,21 @@ class Htj2kCodec(_ImageCodec): @dataclass(frozen=True) class LegacyHtj2kCodec(_ImageCodec): - """HTJ2K under the pre-OpenJPH label; decode-compatible with `Htj2kCodec`.""" + """HTJ2K under the pre-OpenJPH label; decode-compatible with `Htj2kCodec`. + + Read-only by design. The label is registered so existing stores still open, + but new chunks should carry the current name — writing more of them would + spread a codec id we are trying to retire. + """ codec_name = CODEC_HTJ2K_LEGACY + async def _encode_single(self, chunk_array: NDBuffer, chunk_spec: "ArraySpec") -> Buffer | None: + raise NotImplementedError( + f"{CODEC_HTJ2K_LEGACY} is a legacy label kept for reading existing stores; " + f"write with {CODEC_HTJ2K_OPENJPH} instead." + ) + @dataclass(frozen=True) class Jpeg2kCodec(_ImageCodec): diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/pyramids.py b/python/spatialdata-js-util/src/spatialdata_js_util/pyramids.py index 2926214e..087a0716 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/pyramids.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/pyramids.py @@ -97,7 +97,13 @@ def resolve_scale_factors( if levels != "auto": if levels < 1: raise WriterCommandError(f"Pyramid levels must be at least 1, got {levels}.") - return [downscale] * (min(int(levels), MAX_LEVELS) - 1) + # Silently capping an explicit request writes a pyramid the caller did not + # ask for and reports success; the cap is only implicit for `"auto"`. + if levels > MAX_LEVELS: + raise WriterCommandError( + f"Pyramid levels must be at most {MAX_LEVELS}, got {levels}." + ) + return [downscale] * (int(levels) - 1) if min_size < 1: raise WriterCommandError(f"Pyramid min-size must be positive, got {min_size}.") diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/store.py b/python/spatialdata-js-util/src/spatialdata_js_util/store.py index 129908ec..48b2c278 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/store.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/store.py @@ -10,6 +10,7 @@ import json import shutil +from copy import deepcopy from pathlib import Path from typing import Any, Sequence @@ -219,7 +220,7 @@ def _points_element_consolidated_entry( if metadata is not None: entry = metadata.get(f"points/{points_key}") if isinstance(entry, dict): - return json.loads(json.dumps(entry)) + return deepcopy(entry) element_json = _points_root(zarr_path) / points_key / "zarr.json" element_doc = read_json(element_json) return { @@ -268,16 +269,14 @@ def register_points_elements_in_consolidated_metadata( metadata=metadata, ) for key in element_keys: - metadata[f"points/{key}"] = json.loads(json.dumps(template_entry)) - root_json.write_text(json.dumps(doc, indent=2) + "\n") + metadata[f"points/{key}"] = deepcopy(template_entry) + write_json(root_json, doc) def read_points_dataframe(parquet_path: str | Path) -> pd.DataFrame: path = Path(parquet_path) if not path.exists(): raise FileNotFoundError(f"Points Parquet not found: {path}") - if path.is_dir(): - table = ds.dataset(path, format="parquet").to_table() - else: - table = ds.dataset(path, format="parquet").to_table() + # `ds.dataset` handles a single file and a partitioned directory alike. + table = ds.dataset(path, format="parquet").to_table() return table.to_pandas() diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/tables.py b/python/spatialdata-js-util/src/spatialdata_js_util/tables.py index 59a42ffb..ce928ca1 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/tables.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/tables.py @@ -180,7 +180,12 @@ def _convert_table_matrices( report: dict[str, str] = {} with _anndata_write_settings(zarr_format=zarr_format_of(table_path)): - report["X"] = _convert_group_matrix(root, "X", densify=densify) + # `X` is optional in AnnData — a table may carry only `layers`, or only + # annotations. Converting what is there beats refusing the whole table. + if "X" in root: + report["X"] = _convert_group_matrix(root, "X", densify=densify) + else: + report["X"] = "absent" if layers and "layers" in root: layers_group = root["layers"] for name in sorted(layers_group.keys()): @@ -196,9 +201,16 @@ def _convert_table_matrices( def _table_dimensions(table_path: Path) -> tuple[int, int]: + """Report `(n_obs, n_vars)`, or `(0, 0)` for a table with no `X`. + + Only used for the manifest, so an absent matrix is worth reporting as zero + rather than failing a conversion that has nothing to do with dimensions. + """ import zarr root = zarr.open_group(str(table_path), mode="r", use_consolidated=False) + if "X" not in root: + return 0, 0 shape = root["X"].attrs.get("shape") if shape is None: # dense X is a plain array shape = root["X"].shape @@ -231,6 +243,17 @@ def convert_store_tables_to_csc( else: store_path = Path(dest) in_place = False + # The overwrite branch below deletes the destination before copying, so a + # destination that *is* the source — or contains it — would delete the + # store we are about to read. Checked against resolved paths so a symlink + # or `.` in the argument cannot slip past. + resolved_source = source_path.resolve() + resolved_dest = store_path.resolve() + if resolved_dest == resolved_source or resolved_dest in resolved_source.parents: + raise WriterCommandError( + f"Destination would delete the source store: {store_path}\n" + "Pass no destination to convert in place." + ) if store_path.exists(): if not overwrite: raise WriterCommandError( diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py b/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py index f546dc36..0e110de6 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/tui/screens.py @@ -729,7 +729,17 @@ def runner() -> dict[str, Any]: class RecompressImagesScreen(InputFormScreen): """Recompress image rasters — the `images recompress` command.""" - INPUT_ORDER = ("source", "dest", "image-key", "quality", "chunks", "workers") + INPUT_ORDER = ( + "source", + "dest", + "image-key", + "quality", + "chunks", + "workers", + "pyramid-levels", + "pyramid-downscale", + "pyramid-min-size", + ) def compose(self) -> ComposeResult: yield Header() @@ -970,18 +980,39 @@ def runner() -> dict[str, Any]: ) in_place = dest is None + dest_exists = dest is not None and Path(dest).exists() + if dest_exists and not overwrite: + # The run would fail on this deep inside the conversion; say so while + # the form is still on screen and editable. + self.notify( + f"Destination already exists: {dest}\nTick overwrite to replace it.", + severity="error", + ) + return + + scope = "All tables" if tables is None else "Tables: " + ", ".join(tables) + if in_place: + confirm_message = ( + f"Rewrite table matrices in place:\n{source}\n\n{scope}\n\n" + "Values are preserved; only the sparse layout changes. Proceed?" + ) + else: + confirm_message = ( + f"Replace the existing store:\n{dest}\n\n{scope}\n\n" + "Its current contents are deleted before the copy. Proceed?" + ) + + # Anything that destroys existing data is confirmed, whether that is the + # source (in place) or the destination (overwrite). + needs_confirm = in_place or dest_exists task = TaskSpec( command=CommandId.TABLES_TO_CSC, title="Tables to CSC", runner=runner, - requires_confirm=in_place, - confirm_message=( - f"Rewrite table matrices in place:\n{source}\n\n" - f"{'All tables' if tables is None else 'Tables: ' + ', '.join(tables)}\n\n" - "Values are preserved; only the sparse layout changes. Proceed?" - ), + requires_confirm=needs_confirm, + confirm_message=confirm_message, ) - self.app.push_screen(ConfirmScreen(task) if in_place else RunScreen(task)) + self.app.push_screen(ConfirmScreen(task) if needs_confirm else RunScreen(task)) class ConfirmScreen(WriterScreen): diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/verify.py b/python/spatialdata-js-util/src/spatialdata_js_util/verify.py index 1bde1e37..8e3c16b3 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/verify.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/verify.py @@ -208,7 +208,14 @@ def verify_multiscale_parquet(path: str | Path) -> list[VerifyCheck]: checks.append(_check("file_exists", True, str(parquet_path))) - schema_metadata = pq.ParquetFile(parquet_path).schema_arrow.metadata + # A verifier that raises on a corrupt file reports nothing about it; the + # unreadable file is itself the finding, as in `verify_morton_parquet`. + try: + schema_metadata = pq.ParquetFile(parquet_path).schema_arrow.metadata + except Exception as exc: + checks.append(_check("parquet_readable", False, f"failed to read parquet: {exc}")) + return checks + if schema_metadata is None or b"spatialdata_multiscale" not in schema_metadata: checks.append( _check( @@ -302,13 +309,25 @@ def verify_index_permutations_manifest(dest_zarr: str | Path) -> list[VerifyChec checks.append(_check(f"path_{condition_id}", True, str(parquet_path))) tiling_kind = condition.get("tiling_kind") - if tiling_kind == "morton-points" and parquet_path.is_file(): - for morton_check in verify_morton_parquet(parquet_path): + if tiling_kind == "morton-points": + if parquet_path.is_file(): + for morton_check in verify_morton_parquet(parquet_path): + checks.append( + VerifyCheck( + id=f"{condition_id}_{morton_check.id}", + passed=morton_check.passed, + detail=morton_check.detail, + ) + ) + else: + # A directory dataset passes the path check above, so without this + # the manifest would report `all_passed` for a Morton condition + # whose ordering was never looked at. checks.append( - VerifyCheck( - id=f"{condition_id}_{morton_check.id}", - passed=morton_check.passed, - detail=morton_check.detail, + _check( + f"{condition_id}_morton_verified", + False, + f"morton ordering not verified: {parquet_path} is a directory dataset", ) ) diff --git a/python/spatialdata-js-util/tests/test_pyramids.py b/python/spatialdata-js-util/tests/test_pyramids.py index 54c44dfb..5177266b 100644 --- a/python/spatialdata-js-util/tests/test_pyramids.py +++ b/python/spatialdata-js-util/tests/test_pyramids.py @@ -97,6 +97,13 @@ def test_rejects_zero_levels(self) -> None: with pytest.raises(WriterCommandError, match="at least 1"): resolve_scale_factors(element, levels=0) + def test_rejects_more_levels_than_the_cap(self) -> None: + # Clamping instead would build a different pyramid from the one asked + # for and report success. + element = self._Element(c=1, y=4096, x=4096) + with pytest.raises(WriterCommandError, match="at most"): + resolve_scale_factors(element, levels=MAX_LEVELS + 1) + class TestAddPyramids: def test_single_level_store_gains_levels(self, tmp_path: Path) -> None: diff --git a/python/spatialdata-js-util/tests/test_tables.py b/python/spatialdata-js-util/tests/test_tables.py index 6f0a7788..edf139ae 100644 --- a/python/spatialdata-js-util/tests/test_tables.py +++ b/python/spatialdata-js-util/tests/test_tables.py @@ -3,12 +3,14 @@ from __future__ import annotations import json +import shutil from pathlib import Path import numpy as np import pytest from spatialdata_js_util.errors import WriterCommandError +from spatialdata_js_util.store import drop_consolidated_metadata from spatialdata_js_util.tables import ( anndata_to_csc, convert_store_tables_to_csc, @@ -142,6 +144,26 @@ def test_rejects_existing_dest_without_overwrite(self, tmp_path: Path) -> None: with pytest.raises(WriterCommandError, match="already exists"): convert_store_tables_to_csc(store, dest) + def test_refuses_a_destination_that_would_delete_the_source(self, tmp_path: Path) -> None: + # The overwrite branch rmtree's the destination first, so a destination + # equal to (or containing) the source destroys the store being read. + store = _store(tmp_path / "s.zarr") + for dest in (store, store.parent): + with pytest.raises(WriterCommandError, match="delete the source"): + convert_store_tables_to_csc(store, dest, overwrite=True) + assert (store / "tables" / "table").is_dir() + + def test_converts_a_table_with_no_x(self, tmp_path: Path) -> None: + # `X` is optional in AnnData; the layers still convert. + store = _store(tmp_path / "s.zarr") + shutil.rmtree(store / "tables" / "table" / "X") + drop_consolidated_metadata(store / "tables" / "table") + + result = convert_store_tables_to_csc(store) + + assert result.manifest["tables"][0]["matrices"]["X"] == "absent" + assert result.manifest["tables"][0]["n_obs"] == 0 + def test_rejects_unknown_table_key(self, tmp_path: Path) -> None: store = _store(tmp_path / "s.zarr") with pytest.raises(WriterCommandError, match="Unknown table"): @@ -220,8 +242,8 @@ def test_index_permutations_writes_the_points_group(self, tmp_path: Path) -> Non assert (permuted / "points" / "zarr.json").is_file() - def test_refresh_refuses_to_write_orphaned_entries(self, tmp_path: Path) -> None: - """The guard that would have turned this into a loud failure.""" + def test_refresh_reconstructs_a_missing_collection_parent(self, tmp_path: Path) -> None: + """The repair that keeps a store openable when its parent group is absent.""" import json from spatialdata_js_util.store import ( @@ -241,6 +263,26 @@ def test_refresh_refuses_to_write_orphaned_entries(self, tmp_path: Path) -> None assert meta["points"]["node_type"] == "group" assert sd.read_zarr(store) is not None + def test_refresh_refuses_to_write_orphaned_entries(self, monkeypatch, tmp_path: Path) -> None: + """The guard itself — an orphan that reconstruction cannot fill in. + + Reconstruction covers every orphan reachable from a disk walk, so the + `ValueError` branch is only reachable if that ever stops holding. Pinning + it means a change to `_collect_consolidated_metadata` cannot quietly start + writing a listing that makes the store unopenable. + """ + from spatialdata_js_util import store as store_module + + store = self._store_with_points(tmp_path / "src.zarr") + + def _orphaned(*_args, **_kwargs): + return {"points/transcripts": {"node_type": "group", "zarr_format": 3}} + + monkeypatch.setattr(store_module, "_collect_consolidated_metadata", _orphaned) + + with pytest.raises(ValueError, match="orphaned entries"): + store_module.refresh_consolidated_metadata(store) + class TestConversionDoesNotReEncodeTheRestOfTheTable: """Regression: the conversion must touch the matrices and nothing else. diff --git a/python/spatialdata-js-util/tests/test_tui.py b/python/spatialdata-js-util/tests/test_tui.py index 9d6a0e2a..9e2c85fb 100644 --- a/python/spatialdata-js-util/tests/test_tui.py +++ b/python/spatialdata-js-util/tests/test_tui.py @@ -19,6 +19,7 @@ from textual.widgets import Button, Checkbox, Input, ListView # noqa: E402 +from spatialdata_js_util.codecs import htj2k_available # noqa: E402 from spatialdata_js_util.tui.app import WriterApp # noqa: E402 from spatialdata_js_util.tui.models import CommandId # noqa: E402 from spatialdata_js_util.tui.screens import ( # noqa: E402 @@ -199,6 +200,36 @@ async def test_writing_to_a_destination_skips_the_confirm(self, tmp_path: Path) assert sd.read_zarr(dest).tables["table"].X.format == "csc" assert sd.read_zarr(store).tables["table"].X.format == "csr" + @pytest.mark.asyncio + async def test_existing_destination_is_confirmed_before_it_is_replaced( + self, tmp_path: Path + ) -> None: + """Overwriting a destination deletes it, so it is confirmed like an in-place run.""" + store = _store_with_table(tmp_path / "s.zarr") + dest = tmp_path / "csc.zarr" + dest.mkdir() + (dest / "keep.txt").write_text("existing contents") + + app = WriterApp() + async with app.run_test() as pilot: + await pilot.pause() + await _select(pilot, "cmd-tables-to-csc") + app.screen.query_one("#source", Input).value = str(store) + app.screen.query_one("#dest", Input).value = str(dest) + + # Without the overwrite box the form rejects it rather than starting a + # run that would fail deep in the conversion. + app.screen.query_one("#run", Button).press() + await pilot.pause() + assert isinstance(app.screen, TablesToCscScreen) + assert (dest / "keep.txt").is_file() + + app.screen.query_one("#overwrite", Checkbox).value = True + await pilot.pause() + app.screen.query_one("#run", Button).press() + await pilot.pause() + assert isinstance(app.screen, ConfirmScreen) + @pytest.mark.asyncio async def test_missing_source_is_reported_not_run(self, tmp_path: Path) -> None: app = WriterApp() @@ -215,6 +246,10 @@ async def test_missing_source_is_reported_not_run(self, tmp_path: Path) -> None: class TestRecompressImages: @pytest.mark.asyncio + @pytest.mark.skipif( + not htj2k_available(), + reason="No HTJ2K encoder is available in this environment.", + ) async def test_recompresses_losslessly_and_reads_back(self, tmp_path: Path) -> None: source = tmp_path / "src.zarr" pixels = _store_with_image(source) @@ -290,6 +325,10 @@ async def test_existing_destination_requires_the_overwrite_box(self, tmp_path: P class TestRecompressPyramidControls: @pytest.mark.asyncio + @pytest.mark.skipif( + not htj2k_available(), + reason="No HTJ2K encoder is available in this environment.", + ) async def test_pyramid_checkbox_builds_levels(self, tmp_path: Path) -> None: from spatialdata_js_util.pyramids import has_pyramid @@ -316,6 +355,10 @@ async def test_pyramid_checkbox_builds_levels(self, tmp_path: Path) -> None: assert has_pyramid(dest, "images", "morphology") @pytest.mark.asyncio + @pytest.mark.skipif( + not htj2k_available(), + reason="No HTJ2K encoder is available in this environment.", + ) async def test_pyramid_off_by_default(self, tmp_path: Path) -> None: from spatialdata_js_util.pyramids import has_pyramid diff --git a/python/spatialdata-js-util/tests/test_verify.py b/python/spatialdata-js-util/tests/test_verify.py index 2e4ba373..fa528bbd 100644 --- a/python/spatialdata-js-util/tests/test_verify.py +++ b/python/spatialdata-js-util/tests/test_verify.py @@ -6,7 +6,7 @@ import pyarrow as pa import pyarrow.parquet as pq -from spatialdata_js_util.index_permutations import write_index_permutations +from spatialdata_js_util.index_permutations import DEFAULT_CONDITIONS, write_index_permutations from spatialdata_js_util.points import ( MORTON_CODE_2D_COLUMN, build_spatialdata_multiscale_metadata, @@ -152,10 +152,7 @@ def test_verify_index_permutations_manifest(tmp_path) -> None: row_group_size=2, conditions=tuple( condition - for condition in __import__( - "spatialdata_js_util.index_permutations", - fromlist=["DEFAULT_CONDITIONS"], - ).DEFAULT_CONDITIONS + for condition in DEFAULT_CONDITIONS if condition.id in {"canonical", "morton"} ), ) diff --git a/python/spatialdata-js-util/tests/test_zarr_codec.py b/python/spatialdata-js-util/tests/test_zarr_codec.py index 055a4166..7d40db2e 100644 --- a/python/spatialdata-js-util/tests/test_zarr_codec.py +++ b/python/spatialdata-js-util/tests/test_zarr_codec.py @@ -23,6 +23,20 @@ from spatialdata_js_util.codecs.zarr_codec import Htj2kCodec, Jpeg2kCodec + +def _skip_if_codec_unavailable(codec_name: str) -> None: + """Skip only when *this* codec's backend is missing. + + A single HTJ2K gate over a parametrised pair gets both cases wrong: JPEG2K + skips when only `imagecodecs` is present without HTJ2K, and runs into an + ImportError when only the WASM backend is. + """ + if codec_name == CODEC_HTJ2K_OPENJPH: + if not htj2k_available(): + pytest.skip("No HTJ2K backend available.") + return + pytest.importorskip("imagecodecs", reason="imagecodecs is required for JPEG 2000.") + def test_codecs_resolve_from_the_zarr_registry() -> None: """Entry-point registration must work without importing our modules first.""" from zarr.registry import get_codec_class @@ -43,10 +57,10 @@ def test_codec_from_dict_tolerates_unknown_configuration() -> None: assert isinstance(Htj2kCodec.from_dict({"configuration": {"unexpected": 1}}), Htj2kCodec) -@pytest.mark.skipif(not htj2k_available(), reason="No HTJ2K backend available.") @pytest.mark.parametrize("codec_name", [CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K]) def test_zarr_array_round_trips_through_the_codec(tmp_path: Path, codec_name: str) -> None: """A zarr array declaring our codec round-trips through zarr's own pipeline.""" + _skip_if_codec_unavailable(codec_name) y, x = np.mgrid[0:64, 0:48] data = ((x * 5 + y * 3) % 4096).astype(np.uint16) @@ -110,8 +124,9 @@ def test_selected_backend_passes_the_probe(self) -> None: selected = report["selected"] if selected is None: pytest.skip("No HTJ2K backend available.") - if selected == BACKEND_IMAGECODECS: - assert report["backends"][BACKEND_IMAGECODECS]["passes_multicomponent_probe"] + # Whichever backend won, it has to decode the fixture — asserting only for + # `imagecodecs` left the WASM selection unchecked by the probe test. + assert backend_passes_probe(selected), f"selected backend {selected} fails the probe" def test_unknown_backend_never_passes(self) -> None: assert backend_passes_probe("not-a-real-backend") is False @@ -144,11 +159,11 @@ def _write_source(path: Path) -> np.ndarray: ).write(path, overwrite=True) return pixels - @pytest.mark.skipif(not htj2k_available(), reason="No HTJ2K backend available.") @pytest.mark.parametrize("codec_name", [CODEC_HTJ2K_OPENJPH, CODEC_JPEG2K]) def test_lossless_recompressed_store_reads_back_exactly( self, tmp_path: Path, codec_name: str ) -> None: + _skip_if_codec_unavailable(codec_name) sd = pytest.importorskip("spatialdata") from spatialdata_js_util import recompress_spatialdata @@ -167,3 +182,18 @@ def test_lossless_recompressed_store_reads_back_exactly( read_back = np.asarray(sd.read_zarr(dest).images["morphology"].data.compute()) assert np.array_equal(read_back, pixels) + + +def test_legacy_codec_refuses_to_encode() -> None: + """The legacy label is registered for reading only. + + It inherits the encode path, so without an explicit refusal a zarr write + through it would either spread a codec id we are retiring or fail with a + generic "unsupported codec" from two layers down. + """ + import asyncio + + from spatialdata_js_util.codecs.zarr_codec import LegacyHtj2kCodec + + with pytest.raises(NotImplementedError, match="legacy label"): + asyncio.run(LegacyHtj2kCodec()._encode_single(None, None)) From 30c9177333d57203de5957d146c046b748f7018b Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 31 Jul 2026 15:40:38 +0100 Subject: [PATCH 09/10] Make the HTJ2K probe builder independent of the WASM backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `build_htj2k_probe.py` imported `codecs.htj2k_wasm` directly and refused to run without Node.js, so removing the Node/WASM encode path would have left the committed probe fixture unreproducible — a frozen artifact gating every backend admission with no way to regenerate it. It now goes through the backend abstraction (`available_backends()`), which makes the WASM backend one option rather than a requirement. Verified by running it with Node off PATH: it produces a valid fixture from imagecodecs alone. That change also fixes what the fixture proved. The old script encoded and round-tripped with the same backend, which only shows self-consistency: a backend mis-ordering components in both directions would have passed. The builder now requires *every* available backend to decode the codestream exactly, so with two installed the fixture is evidence they agree — which is the property that admitting a new backend actually needs. Which backend encoded it is recorded in `multicomponent.json` and reported by `backend_report()`, because a probe result cannot be read correctly without it. The docstring claim that the fixture is "produced by the WASM encoder" would have silently outdated itself on the first regeneration elsewhere. The committed fixture is unchanged: rebuilding reproduces it byte for byte. Worth noting that imagecodecs and openjph-wasm produce byte-identical codestreams for this input. Co-Authored-By: Claude Opus 5 --- python/spatialdata-js-util/pyproject.toml | 1 + .../scripts/build_htj2k_probe.py | 86 +++++++++++++++---- .../spatialdata_js_util/codecs/backends.py | 41 ++++++++- .../codecs/probe/multicomponent.json | 3 + .../tests/test_zarr_codec.py | 24 +++++- 5 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 python/spatialdata-js-util/src/spatialdata_js_util/codecs/probe/multicomponent.json diff --git a/python/spatialdata-js-util/pyproject.toml b/python/spatialdata-js-util/pyproject.toml index a1348434..79d5ad5e 100644 --- a/python/spatialdata-js-util/pyproject.toml +++ b/python/spatialdata-js-util/pyproject.toml @@ -63,6 +63,7 @@ where = ["src"] spatialdata_js_util = [ "codecs/probe/*.j2c", "codecs/probe/*.npy", + "codecs/probe/*.json", "codecs/vendor/*.mjs", "codecs/vendor/openjph/*", "codecs/vendor/openjph/wasm/*", diff --git a/python/spatialdata-js-util/scripts/build_htj2k_probe.py b/python/spatialdata-js-util/scripts/build_htj2k_probe.py index b4c0a4c3..58ed2128 100644 --- a/python/spatialdata-js-util/scripts/build_htj2k_probe.py +++ b/python/spatialdata-js-util/scripts/build_htj2k_probe.py @@ -2,18 +2,32 @@ """Regenerate the committed multi-component HTJ2K probe fixture. The fixture gates whether an HTJ2K backend may be used (see -`spatialdata_js_util.codecs.backends`). It is deliberately produced by the -**openjph-wasm** backend, so passing the probe means "this backend can read what -our writer produces", not merely "this backend is self-consistent". +`spatialdata_js_util.codecs.backends`). It is a codestream plus the exact samples +it must decode to — and those samples are computed here in numpy, so the ground +truth never comes from a codec. -Requires Node.js and the vendored WASM assets: +What the fixture proves depends on *which* backend encoded it: +- encoded by one backend and decoded by another, it proves the two agree, which + is what admitting a new backend needs; +- encoded and decoded by the same single backend, it only shows that backend is + self-consistent. One that mis-ordered components in both directions would pass. + +So this script encodes with one backend and then requires **every** available +backend to decode the result exactly, recording which backend produced it in +`multicomponent.json`. With only one backend installed it still writes the +fixture, but says plainly what the fixture is worth in that case. + +Run with as many backends installed as possible: + + # optional, for the openjph-wasm backend node scripts/vendor-openjph-for-python.mjs uv run --directory python/spatialdata-js-util python scripts/build_htj2k_probe.py """ from __future__ import annotations +import json import sys from pathlib import Path @@ -35,35 +49,71 @@ def probe_volume() -> np.ndarray: def main() -> int: - from spatialdata_js_util.codecs.htj2k_wasm import ( - decode_htj2k_wasm, - encode_htj2k_wasm, - wasm_encode_available, + from spatialdata_js_util.codecs.backends import ( + BACKEND_OPENJPH_WASM, + available_backends, ) - if not wasm_encode_available(): + backends = available_backends() + if not backends: print( - "openjph-wasm backend unavailable. Ensure Node.js is on PATH and run\n" - " node scripts/vendor-openjph-for-python.mjs", + "No HTJ2K backend available. Install `imagecodecs` with HTJ2K support, or put\n" + "Node.js on PATH and run `node scripts/vendor-openjph-for-python.mjs`.", file=sys.stderr, ) return 1 - volume = probe_volume() - codestream = encode_htj2k_wasm(volume, reversible=True) + # Prefer the WASM encoder while it exists: it is the same build the browser + # reader uses, so agreement with it is the property our stores actually need. + # Any other available backend is a valid producer once that is gone. + encoder = next( + (backend for backend in backends if backend.name == BACKEND_OPENJPH_WASM), + backends[0], + ) - roundtrip = decode_htj2k_wasm(codestream).reshape(volume.shape) - if not np.array_equal(roundtrip, volume): - print("Refusing to write a probe fixture the encoder cannot round-trip.", file=sys.stderr) - return 1 + volume = probe_volume() + codestream = encoder.encode(volume, reversible=True, quality=0.0) + + for backend in backends: + try: + decoded = backend.decode(codestream) + except Exception as exc: # noqa: BLE001 - any failure disqualifies the fixture + print( + f"Refusing to write a probe fixture {backend.name} cannot decode: {exc}", + file=sys.stderr, + ) + return 1 + if decoded.shape != volume.shape or not np.array_equal(decoded, volume): + print( + f"Refusing to write a probe fixture {backend.name} decodes incorrectly " + f"(got shape {decoded.shape}, expected {volume.shape}).", + file=sys.stderr, + ) + return 1 PROBE_DIR.mkdir(parents=True, exist_ok=True) (PROBE_DIR / "multicomponent.j2c").write_bytes(codestream) np.save(PROBE_DIR / "multicomponent.npy", volume) + (PROBE_DIR / "multicomponent.json").write_text( + json.dumps({"encoder": encoder.name}, indent=2, sort_keys=True) + "\n" + ) + + cross_checked = sorted(backend.name for backend in backends if backend.name != encoder.name) print( f"Wrote probe fixture to {PROBE_DIR}: " - f"{len(codestream)} byte codestream, {volume.shape} {volume.dtype} expected samples" + f"{len(codestream)} byte codestream, {volume.shape} {volume.dtype} expected samples, " + f"encoded by {encoder.name}" ) + if cross_checked: + print(f"Cross-checked against: {', '.join(cross_checked)}") + else: + print( + f"WARNING: {encoder.name} was the only backend available, so this fixture shows\n" + " that it is self-consistent, not that it agrees with another decoder.\n" + " Regenerate with a second backend installed before relying on it to\n" + " admit one.", + file=sys.stderr, + ) return 0 diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/backends.py b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/backends.py index cad502b6..238ca8cc 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/backends.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/backends.py @@ -17,9 +17,15 @@ `docs/multi-component-codec-findings.md`). That defect is specific to that build, not to OpenJPH — but "a build of OpenJPH" is not on its own evidence of correctness. So `imagecodecs` is not trusted on reputation: it is admitted only -if it decodes a committed multi-component codestream, produced by the WASM -encoder, to the exact expected samples. The probe runs once per process and the -backend is rejected on any mismatch. +if it decodes a committed multi-component codestream to the exact expected +samples. The probe runs once per process and the backend is rejected on any +mismatch. + +The fixture's expected samples are generated in numpy rather than by a decoder, +so the ground truth is codec-independent. Which backend *encoded* the codestream +is recorded alongside it and reported by `backend_report()` — probing a backend +against a codestream it produced itself only shows self-consistency, so that +provenance is part of reading the result. See `scripts/build_htj2k_probe.py`. Set ``SPATIALDATA_JS_UTIL_HTJ2K_BACKEND=imagecodecs|openjph-wasm`` to pin one backend explicitly (the probe still gates ``imagecodecs``). @@ -27,6 +33,7 @@ from __future__ import annotations +import json import os from functools import lru_cache from pathlib import Path @@ -41,6 +48,7 @@ _PROBE_DIR = Path(__file__).resolve().parent / "probe" _PROBE_CODESTREAM = _PROBE_DIR / "multicomponent.j2c" _PROBE_EXPECTED = _PROBE_DIR / "multicomponent.npy" +_PROBE_PROVENANCE = _PROBE_DIR / "multicomponent.json" class Htj2kBackend(Protocol): @@ -140,6 +148,30 @@ def probe_fixture() -> tuple[bytes, np.ndarray]: return _PROBE_CODESTREAM.read_bytes(), np.load(_PROBE_EXPECTED) +def probe_encoder() -> str | None: + """Which backend encoded the committed fixture, or `None` if unrecorded. + + A backend probed against a codestream it produced itself has only shown that + it is self-consistent, so this is needed to read a probe result correctly. + """ + if not _PROBE_PROVENANCE.is_file(): + return None + try: + encoder = json.loads(_PROBE_PROVENANCE.read_text()).get("encoder") + except (OSError, ValueError): + return None + return encoder if isinstance(encoder, str) else None + + +def available_backends() -> list[Htj2kBackend]: + """Every installed backend, in preference order. + + Exposed for the probe builder, which has to encode with one backend and + check the result against the others rather than take the single selected one. + """ + return [_BACKENDS[name] for name in _PREFERENCE if _BACKENDS[name].available()] + + @lru_cache(maxsize=None) def backend_passes_probe(name: str) -> bool: """Whether *name* decodes the committed multi-component fixture exactly. @@ -205,6 +237,9 @@ def backend_report() -> dict[str, Any]: selected = resolve_backend() return { "selected": selected.name if selected else None, + # Reading a probe result needs to know who produced the codestream: a + # backend that encoded it has only proved self-consistency by decoding it. + "probe_encoder": probe_encoder(), "backends": { name: { "available": backend.available(), diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/codecs/probe/multicomponent.json b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/probe/multicomponent.json new file mode 100644 index 00000000..291225d7 --- /dev/null +++ b/python/spatialdata-js-util/src/spatialdata_js_util/codecs/probe/multicomponent.json @@ -0,0 +1,3 @@ +{ + "encoder": "openjph-wasm" +} diff --git a/python/spatialdata-js-util/tests/test_zarr_codec.py b/python/spatialdata-js-util/tests/test_zarr_codec.py index 7d40db2e..3e2aac9e 100644 --- a/python/spatialdata-js-util/tests/test_zarr_codec.py +++ b/python/spatialdata-js-util/tests/test_zarr_codec.py @@ -17,13 +17,14 @@ from spatialdata_js_util.codecs.backends import ( BACKEND_IMAGECODECS, BACKEND_OPENJPH_WASM, + available_backends, backend_passes_probe, + probe_encoder, probe_fixture, ) from spatialdata_js_util.codecs.zarr_codec import Htj2kCodec, Jpeg2kCodec - def _skip_if_codec_unavailable(codec_name: str) -> None: """Skip only when *this* codec's backend is missing. @@ -37,6 +38,7 @@ def _skip_if_codec_unavailable(codec_name: str) -> None: return pytest.importorskip("imagecodecs", reason="imagecodecs is required for JPEG 2000.") + def test_codecs_resolve_from_the_zarr_registry() -> None: """Entry-point registration must work without importing our modules first.""" from zarr.registry import get_codec_class @@ -131,6 +133,26 @@ def test_selected_backend_passes_the_probe(self) -> None: def test_unknown_backend_never_passes(self) -> None: assert backend_passes_probe("not-a-real-backend") is False + def test_probe_records_which_backend_encoded_it(self) -> None: + """A backend probed against its own output has only shown self-consistency. + + So the provenance travels with the fixture and is reported, rather than + living in a docstring that a regeneration would silently outdate. + """ + encoder = probe_encoder() + assert encoder in {BACKEND_IMAGECODECS, BACKEND_OPENJPH_WASM} + assert backend_report()["probe_encoder"] == encoder + + def test_available_backends_is_a_subset_in_preference_order(self) -> None: + # The probe builder encodes with one backend and checks the rest, so it + # needs all of them rather than the single selected one. + names = [backend.name for backend in available_backends()] + assert names == [ + name for name in (BACKEND_IMAGECODECS, BACKEND_OPENJPH_WASM) if name in names + ] + for name in names: + assert backend_report()["backends"][name]["available"] + @pytest.mark.parametrize("name", [BACKEND_IMAGECODECS, BACKEND_OPENJPH_WASM]) def test_available_backends_decode_our_codestream(self, name: str) -> None: """Whatever backends exist here must agree with the committed fixture.""" From 660180e7173c5e575324b890afbfd398af5dc33d Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Fri, 31 Jul 2026 16:14:20 +0100 Subject: [PATCH 10/10] Fill in implicit parents when registering points siblings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `register_points_elements_in_consolidated_metadata` adds `points/` to the root listing but never checked that a `points` entry exists. Where the source store has none — the broken-writer case this module exists to handle — the result is an orphaned entry, and zarr rejects the *whole* store rather than the one element. Reproduced: `GroupNotFoundError` at the root, with every element gone, not just the new sibling. This is the failure `refresh_consolidated_metadata` already guards against. It was reachable here because this function edits the map directly instead of rebuilding it from disk, so it never went through that guard. The parent-filling loop is now shared with `_collect_consolidated_metadata`, and this function runs the same orphan check before writing, so neither writer of the listing can produce one. Co-Authored-By: Claude Opus 5 --- .../src/spatialdata_js_util/store.py | 25 +++++++++++++- .../spatialdata-js-util/tests/test_tables.py | 34 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/python/spatialdata-js-util/src/spatialdata_js_util/store.py b/python/spatialdata-js-util/src/spatialdata_js_util/store.py index 48b2c278..f93b761f 100644 --- a/python/spatialdata-js-util/src/spatialdata_js_util/store.py +++ b/python/spatialdata-js-util/src/spatialdata_js_util/store.py @@ -76,13 +76,23 @@ def _collect_consolidated_metadata(store_path: Path, *, zarr_format: int = 3) -> rel = meta_path.parent.relative_to(store_path).as_posix() metadata[rel] = read_json(meta_path) + _fill_implicit_parents(metadata, zarr_format) + return metadata + + +def _fill_implicit_parents(metadata: dict[str, Any], zarr_format: int) -> None: + """Add a group entry for every ancestor path that has none, in place. + + Listing `points/transcripts` without `points` leaves an orphan that zarr + cannot attach to the hierarchy, and it rejects the *whole* store rather than + the one element — so every writer of this map has to close the gap. + """ for rel in list(metadata): parts = rel.split("/") for depth in range(1, len(parts)): parent = "/".join(parts[:depth]) if parent not in metadata: metadata[parent] = _implicit_group(zarr_format) - return metadata def consolidated_metadata_orphans(metadata: dict[str, Any]) -> list[str]: @@ -270,6 +280,19 @@ def register_points_elements_in_consolidated_metadata( ) for key in element_keys: metadata[f"points/{key}"] = deepcopy(template_entry) + + # Adding `points/` to a listing with no `points` entry orphans it, and + # zarr then refuses the whole store — the same failure `refresh_consolidated_ + # metadata` guards against, reachable here because this writes the map + # directly. A source store missing the parent is exactly the case this module + # exists to handle, so it cannot be assumed away. + _fill_implicit_parents(metadata, int(doc.get("zarr_format", 3))) + orphans = consolidated_metadata_orphans(metadata) + if orphans: + raise ValueError( + f"Refusing to write consolidated metadata with orphaned entries for {store_path}: " + f"{', '.join(orphans)}" + ) write_json(root_json, doc) diff --git a/python/spatialdata-js-util/tests/test_tables.py b/python/spatialdata-js-util/tests/test_tables.py index edf139ae..518f9e82 100644 --- a/python/spatialdata-js-util/tests/test_tables.py +++ b/python/spatialdata-js-util/tests/test_tables.py @@ -263,6 +263,40 @@ def test_refresh_reconstructs_a_missing_collection_parent(self, tmp_path: Path) assert meta["points"]["node_type"] == "group" assert sd.read_zarr(store) is not None + def test_registering_siblings_fills_in_a_missing_points_parent(self, tmp_path: Path) -> None: + """The same orphan, reached by the other writer of the listing. + + `register_points_elements_in_consolidated_metadata` edits the map + directly rather than rebuilding it from disk, so it does not go through + `refresh_consolidated_metadata`'s guard — and a source store whose + listing omits `points` is the case this module exists for. + """ + import shutil + + from spatialdata_js_util.store import ( + consolidated_metadata_orphans, + register_points_elements_in_consolidated_metadata, + ) + + store = self._store_with_points(tmp_path / "src.zarr") + shutil.copytree( + store / "points" / "transcripts", store / "points" / "transcripts_morton" + ) + + root_json = store / "zarr.json" + doc = json.loads(root_json.read_text()) + doc["consolidated_metadata"]["metadata"].pop("points", None) + root_json.write_text(json.dumps(doc, indent=2)) + + register_points_elements_in_consolidated_metadata( + store, ["transcripts_morton"], template_key="transcripts" + ) + + meta = json.loads(root_json.read_text())["consolidated_metadata"]["metadata"] + assert consolidated_metadata_orphans(meta) == [] + assert meta["points"]["node_type"] == "group" + assert sorted(sd.read_zarr(store).points) == ["transcripts", "transcripts_morton"] + def test_refresh_refuses_to_write_orphaned_entries(self, monkeypatch, tmp_path: Path) -> None: """The guard itself — an orphan that reconstruction cannot fill in.